#!/usr/bin/env bash
set -euo pipefail
runtime_pkg_args=(--pkg phpef-runtime-core --pkg phpef-runtime-embed)
if [ -n "${PHPEF_RUNTIME_NATIVE_PACKAGES:-}" ]; then
    old_ifs=$IFS; IFS=:
    for native_pkg in $PHPEF_RUNTIME_NATIVE_PACKAGES; do
        [ -n "$native_pkg" ] && runtime_pkg_args+=(--pkg "$native_pkg")
    done
    IFS=$old_ifs
elif [ -n "${PHPEF_RUNTIME_NATIVE_VAPIS:-}" ]; then
    old_ifs=$IFS; IFS=:
    for native_vapi in $PHPEF_RUNTIME_NATIVE_VAPIS; do
        [ -n "$native_vapi" ] || continue
        native_pkg=$(basename "$native_vapi" .vapi)
        runtime_pkg_args+=(--pkg "$native_pkg")
    done
    IFS=$old_ifs
fi

build_root=$1
test_dir=$2
php_bin=${3:-php}
valac_bin=${4:-valac}
optimizer_profile=${5:-typing-only}

cli_dir="$build_root/src/cli"
php2ast="$cli_dir/phpefc-php2ast"
phpast2phpir="$cli_dir/phpefc-phpast2phpir"
phpir2valair="$cli_dir/phpefc-phpir2valair"
valair2vala="$cli_dir/phpefc-valair2vala"
valair_optimize_typing="$cli_dir/phpefc-valair-optimize-typing"
valair_optimize_library="$cli_dir/phpefc-valair-optimize-library"
valair_optimize_fastpath_runtime="$cli_dir/phpefc-valair-optimize-fastpath-runtime"
add_resource_sections="$cli_dir/phpefc-add-resource-sections"

for tool in "$php_bin" "$valac_bin" "$php2ast" "$phpast2phpir" "$phpir2valair" "$valair2vala" "$add_resource_sections"; do
    if ! command -v "$tool" >/dev/null 2>&1 && [ ! -x "$tool" ]; then
        echo "missing tool: $tool" >&2
        exit 99
    fi
done

case "$optimizer_profile" in
    standard|all|all-optimizers)
        optimizer_profile=standard
        optimizer_passes=(typing-zval2static fastpath-runtime library-embed2native typing-zval2static)
        required_optimizer_tools=("$valair_optimize_typing" "$valair_optimize_library" "$valair_optimize_fastpath_runtime")
        ;;
    no-optimizer|none)
        optimizer_profile=no-optimizer
        optimizer_passes=()
        required_optimizer_tools=()
        ;;
    typing-only|typing)
        optimizer_profile=typing-only
        optimizer_passes=(typing-zval2static)
        required_optimizer_tools=("$valair_optimize_typing")
        ;;
    library-only|library)
        optimizer_profile=library-only
        optimizer_passes=(library-embed2native)
        required_optimizer_tools=("$valair_optimize_library")
        ;;
    runtime-only|runtime|fastpath-only|fastpath-runtime-only|fastpath)
        optimizer_profile=runtime-only
        optimizer_passes=(fastpath-runtime)
        required_optimizer_tools=("$valair_optimize_fastpath_runtime")
        ;;
    typing-library|library-typing)
        optimizer_profile=typing-library
        optimizer_passes=(typing-zval2static library-embed2native typing-zval2static)
        required_optimizer_tools=("$valair_optimize_typing" "$valair_optimize_library")
        ;;
    typing-runtime|runtime-typing|typing-fastpath|fastpath-typing)
        optimizer_profile=typing-runtime
        optimizer_passes=(typing-zval2static fastpath-runtime typing-zval2static)
        required_optimizer_tools=("$valair_optimize_typing" "$valair_optimize_fastpath_runtime")
        ;;
    library-runtime|runtime-library|library-fastpath|fastpath-library)
        optimizer_profile=library-runtime
        optimizer_passes=(fastpath-runtime library-embed2native)
        required_optimizer_tools=("$valair_optimize_library" "$valair_optimize_fastpath_runtime")
        ;;
    *)
        echo "unknown optimizer profile: $optimizer_profile" >&2
        exit 2
        ;;
esac

for tool in "${required_optimizer_tools[@]}"; do
    if ! command -v "$tool" >/dev/null 2>&1 && [ ! -x "$tool" ]; then
        echo "missing tool: $tool" >&2
        exit 99
    fi
done

if [ ! -d "$test_dir" ]; then
    echo "missing compare test directory: $test_dir" >&2
    exit 2
fi
if [ ! -f "$test_dir/main.php" ]; then
    echo "compare test directory must contain main.php: $test_dir" >&2
    exit 2
fi

work=${MESON_TEST_TMPDIR:-}
if [ -z "$work" ]; then
    work=$(mktemp -d)
    trap 'rm -rf "$work"' EXIT
fi
mkdir -p "$work/src" "$work/build"

# Run both PHP and PHPEFC from an isolated copy of the compare-test directory so
# relative include paths behave like `cd test-dir && php main.php`, without
# writing build artifacts into the source tree.
cp -R "$test_dir"/. "$work/src"/

php_out="$work/php.combined"
phpefc_out="$work/phpefc.combined"

normalize_compare_output() {
    local input=$1
    local output=$2
    python3 - "$input" "$output" <<'PY'
import re
import sys

src, dst = sys.argv[1], sys.argv[2]
out = []
in_stack = False
skip_blank_after_fatal = False

DIAGNOSTIC_RE = re.compile(
    r'^(?:(PHP|PHPEF|PHPEFC)\s+)?'
    r'(Warning|Notice|Deprecated|Strict Standards|Recoverable fatal error):\s+(.+)$'
)


def normalize_nonfatal_diagnostic(line):
    m = DIAGNOSTIC_RE.match(line)
    if not m: return None
    severity = m.group(2)
    # Compare tests validate observable program behavior. Until PHPEF has a
    # generic source-aware diagnostic layer, keep non-fatal diagnostics from
    # failing solely because wording/path/line details differ from PHP CLI.
    return f'PHP {severity}: <diagnostic>'


def normalize_fatal(line):
    line = re.sub(r'^(?:PHP|PHPEF|PHPEFC) Fatal error:\s+', 'PHP Fatal error: ', line)
    if not line.startswith('PHP Fatal error: '):
        return None
    line = re.sub(r' in .+?:\d+$', '', line)
    m = re.match(r'^PHP Fatal error:\s+Uncaught ([A-Za-z_\\][A-Za-z0-9_\\]*)\b', line)
    if m: return 'PHP Fatal error: Uncaught ' + m.group(1)
    m = re.match(r'^PHP Fatal error:\s+([^:]+):', line)
    if m: return 'PHP Fatal error: ' + m.group(1)
    return line

with open(src, 'r', encoding='utf-8', errors='replace') as fh:
    for raw in fh:
        line = raw.rstrip('\n')

        nonfatal = normalize_nonfatal_diagnostic(line)
        if nonfatal is not None:
            out.append(nonfatal)
            in_stack = False
            skip_blank_after_fatal = False
            continue

        fatal = normalize_fatal(line)
        if fatal is not None:
            out.append(fatal)
            in_stack = False
            skip_blank_after_fatal = True
            continue

        if skip_blank_after_fatal and line == '':
            continue
        skip_blank_after_fatal = False

        if line in ('Stack trace:', 'Stacktrace at throw-time:', 'Stacktrace at catch-time:'):
            out.append('Stack trace:')
            in_stack = True
            continue

        if in_stack and re.match(r'^\s*thrown in .+ on line \d+$', line):
            continue

        if in_stack and line.startswith('    in '):
            continue

        if in_stack:
            m = re.match(r'^#\d+ \{main\}$', line)
            if m:
                out.append('#FRAME {main}')
                continue
            m = re.match(r"^#\d+ in '([^']+)'(?: at .*)?$", line)
            if m:
                out.append('#FRAME ' + m.group(1))
                continue
            m = re.match(r"^#\d+ .+\): ([A-Za-z_][A-Za-z0-9_]*)\(.*\)$", line)
            if m:
                out.append('#FRAME ' + m.group(1))
                continue
            m = re.match(r'^#\d+ (.+)$', line)
            if m:
                out.append('#FRAME ' + m.group(1))
                continue

        out.append(line)

with open(dst, 'w', encoding='utf-8') as fh:
    fh.write('\n'.join(out))
    if out:
        fh.write('\n')
PY
}

set +e
(
    cd "$work/src"
    "$php_bin" main.php
) >"$php_out" 2>&1
php_rc=$?
set -e

mapfile -t php_files < <(cd "$work/src" && find . -maxdepth 1 -type f -name '*.php' -printf '%P\n' | LC_ALL=C sort)
if [ "${#php_files[@]}" -eq 0 ]; then
    echo "compare test has no .php files: $test_dir" >&2
    exit 2
fi

ast_for() { printf '%s/%s.phpast' "$work/build" "${1//[^A-Za-z0-9_]/_}"; }
phpir_for() { printf '%s/%s.phpir' "$work/build" "${1//[^A-Za-z0-9_]/_}"; }
valair_for() { printf '%s/%s.valair' "$work/build" "${1//[^A-Za-z0-9_]/_}"; }
opt_valair_for() { printf '%s/%s.valair.afteropt.%s' "$work/build" "${1//[^A-Za-z0-9_]/_}" "$2"; }
vala_for() { printf '%s/%s.vala' "$work/build" "${1//[^A-Za-z0-9_]/_}"; }

compile_rc=0
{
    cd "$work/src"

    for src in "${php_files[@]}"; do
        "$php2ast" "$src" "$(ast_for "$src")"
    done

    for src in "${php_files[@]}"; do
        args=("$(ast_for "$src")" "$(phpir_for "$src")")
        for other in "${php_files[@]}"; do
            if [ "$other" != "$src" ]; then
                args+=(--external-phpast "$(ast_for "$other")")
            fi
        done
        "$phpast2phpir" "${args[@]}"
    done

    for src in "${php_files[@]}"; do
        args=("$(phpir_for "$src")" "$(valair_for "$src")")
        for other in "${php_files[@]}"; do
            if [ "$other" != "$src" ]; then
                args+=(--external-phpir "$(phpir_for "$other")")
            fi
        done
        "$phpir2valair" "${args[@]}"
    done

    vala_files=()
    for src in "${php_files[@]}"; do
        valair_input="$(valair_for "$src")"
        optimized_valair="$valair_input"
        pass_index=0
        for optimizer_pass in "${optimizer_passes[@]}"; do
            pass_index=$((pass_index + 1))
            pass_output="$(opt_valair_for "$src" "${optimizer_profile}.${pass_index}.${optimizer_pass}")"
            case "$optimizer_pass" in
                typing-zval2static)
                    if [ "$pass_index" -lt "${#optimizer_passes[@]}" ]; then
                        "$valair_optimize_typing" --suppress-warning "$optimized_valair" "$pass_output"
                    else
                        "$valair_optimize_typing" "$optimized_valair" "$pass_output"
                    fi
                    ;;
                library-embed2native)
                    "$valair_optimize_library" "$optimized_valair" "$pass_output"
                    ;;
                fastpath-runtime)
                    "$valair_optimize_fastpath_runtime" "$optimized_valair" "$pass_output"
                    ;;
                *)
                    echo "unknown optimizer pass in profile $optimizer_profile: $optimizer_pass" >&2
                    exit 2
                    ;;
            esac
            optimized_valair="$pass_output"
        done

        if [ "$src" = "main.php" ]; then
            "$valair2vala" "$optimized_valair" "$(vala_for "$src")"
        else
            "$valair2vala" "$optimized_valair" "$(vala_for "$src")" --no-main
        fi
        vala_files+=("$(vala_for "$src")")
    done

    valac_extra_args=()
    if [ -n "${PHPEF_RUNTIME_DEV_LIBDIR:-}" ]; then
        valac_extra_args+=("-X" "-Wl,-rpath,$PHPEF_RUNTIME_DEV_LIBDIR")
    fi
    if [ -n "${PHPEF_RUNTIME_VAPIDIR:-}" ]; then
        valac_extra_args+=("--vapidir=$PHPEF_RUNTIME_VAPIDIR")
    else
        if [ -n "${PHPEF_RUNTIME_CORE_VAPI:-}" ]; then
            valac_extra_args+=("--vapidir=$(dirname "$PHPEF_RUNTIME_CORE_VAPI")")
        fi
        if [ -n "${PHPEF_RUNTIME_EMBED_VAPI:-}" ]; then
            valac_extra_args+=("--vapidir=$(dirname "$PHPEF_RUNTIME_EMBED_VAPI")")
        fi
        if [ -n "${PHPEF_RUNTIME_NATIVE_VAPIS:-}" ]; then
            old_ifs=$IFS; IFS=:
            for native_vapi in $PHPEF_RUNTIME_NATIVE_VAPIS; do
                [ -n "$native_vapi" ] || continue
                valac_extra_args+=("--vapidir=$(dirname "$native_vapi")")
            done
            IFS=$old_ifs
        fi
    fi

    "$valac_bin" "${valac_extra_args[@]}" "${runtime_pkg_args[@]}" "${vala_files[@]}" -o "$work/build/phpefc-main.raw"
    resource_phpirs=()
    for src in "${php_files[@]}"; do
        resource_phpirs+=("$(phpir_for "$src")")
    done
    "$add_resource_sections" "$work/build/phpefc-main.raw" "$work/build/phpefc-main" "$work/build/phpefc-main.phpef-resource-index.bin" "$work/build/phpefc-main.phpef-resource-index.txt" "${resource_phpirs[@]}"
} >"$work/phpefc.compile" 2>&1 || compile_rc=$?

if [ "$compile_rc" -ne 0 ]; then
    {
        printf 'PHPEFC compile failed with exit code %s\n' "$compile_rc"
        cat "$work/phpefc.compile"
    } >"$phpefc_out"
    phpefc_rc=$compile_rc
else
    expected_sections="$test_dir/expected_elf_sections.txt"
    if [ -f "$expected_sections" ]; then
        if ! command -v readelf >/dev/null 2>&1; then
            echo "missing tool: readelf" >&2
            exit 99
        fi
        readelf -SW "$work/build/phpefc-main" >"$work/phpefc.sections"
        while IFS= read -r expected_section; do
            [ -n "$expected_section" ] || continue
            case "$expected_section" in
                \#*) continue ;;
                *\*) grep -Fq "${expected_section%\*}" "$work/phpefc.sections" || { echo "missing ELF section prefix: $expected_section" >&2; exit 1; } ;;
                *) grep -Fq "$expected_section" "$work/phpefc.sections" || { echo "missing ELF section: $expected_section" >&2; exit 1; } ;;
            esac
        done <"$expected_sections"
        if command -v strip >/dev/null 2>&1; then
            cp "$work/build/phpefc-main" "$work/build/phpefc-main.stripped"
            strip --strip-debug "$work/build/phpefc-main.stripped"
            readelf -SW "$work/build/phpefc-main.stripped" >"$work/phpefc.stripped.sections"
            while IFS= read -r expected_section; do
                [ -n "$expected_section" ] || continue
                case "$expected_section" in
                    \#*) continue ;;
                    *\*) grep -Fq "${expected_section%\*}" "$work/phpefc.stripped.sections" || { echo "stripped binary missing ELF section prefix: $expected_section" >&2; exit 1; } ;;
                    *) grep -Fq "$expected_section" "$work/phpefc.stripped.sections" || { echo "stripped binary missing ELF section: $expected_section" >&2; exit 1; } ;;
                esac
            done <"$expected_sections"
        fi
    fi
    set +e
    (
        cd "$work/src"
        if [ -n "${PHPEF_RUNTIME_DEV_LIBDIR:-}" ]; then
            LD_LIBRARY_PATH="$PHPEF_RUNTIME_DEV_LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$work/build/phpefc-main"
        else
            "$work/build/phpefc-main"
        fi
    ) >"$phpefc_out" 2>&1
    phpefc_rc=$?
    set -e
fi

php_normalized="$work/php.normalized"
phpefc_normalized="$work/phpefc.normalized"
normalize_compare_output "$php_out" "$php_normalized"
normalize_compare_output "$phpefc_out" "$phpefc_normalized"

expected_diagnostics="$test_dir/expected_diagnostics.txt"
if [ -f "$expected_diagnostics" ]; then
    while IFS= read -r expected_diagnostic; do
        [ -n "$expected_diagnostic" ] || continue
        case "$expected_diagnostic" in \#*) continue ;; esac
        grep -Fq "$expected_diagnostic" "$php_normalized" || { echo "missing expected PHP diagnostic: $expected_diagnostic" >&2; cat "$php_normalized" >&2; exit 1; }
        grep -Fq "$expected_diagnostic" "$phpefc_normalized" || { echo "missing expected PHPEFC diagnostic: $expected_diagnostic" >&2; cat "$phpefc_normalized" >&2; exit 1; }
    done <"$expected_diagnostics"
fi

if [ "$php_rc" -ne "$phpefc_rc" ] || ! cmp -s "$php_normalized" "$phpefc_normalized"; then
    echo "compare test mismatch: $(basename "$test_dir")" >&2
    echo "PHP exit:    $php_rc" >&2
    echo "PHPEFC exit: $phpefc_rc" >&2
    echo "--- PHP combined stdout/stderr ---" >&2
    cat "$php_out" >&2
    echo "--- PHPEFC combined stdout/stderr ---" >&2
    cat "$phpefc_out" >&2
    echo "--- normalized unified diff ---" >&2
    diff -u "$php_normalized" "$phpefc_normalized" >&2 || true
    echo "--- raw unified diff ---" >&2
    diff -u "$php_out" "$phpefc_out" >&2 || true
    exit 1
fi

printf 'ok - compare output matches PHP CLI for %s\n' "$(basename "$test_dir")"
