<?php

function cli_error(string $message): void
{
    $stderr = fopen('php://stderr', 'w');
    if ($stderr !== false) {
        fwrite($stderr, $message);
        fclose($stderr);
        return;
    }
    echo $message;
}

function cli_notice(string $message): void
{
    file_put_contents("php://stderr", $message, FILE_APPEND);
}

function usage(string $argv0): void
{
    cli_error("Usage: {$argv0} <input-elf> <output-elf> <resource-index-output> <resource-debug-map-output> <input.phpir> [input.phpir ...]\n");
}

function bundle_payload(string $path): mixed
{
    $bundle = @file_get_contents($path);
    if ($bundle === false) {
        throw new RuntimeException("Could not read PHP-IR file: {$path}");
    }
    $wrapper = @unserialize($bundle, ['allowed_classes' => false]);
    if (!is_array($wrapper) || !isset($wrapper['embed_serialized']) || !is_string($wrapper['embed_serialized'])) {
        throw new RuntimeException("Invalid PHP-IR serialized bundle: {$path}");
    }
    $serialized = $wrapper['embed_serialized'];
    if (($wrapper['embed_serialized_encoding'] ?? null) === 'base64') {
        $decoded = base64_decode($serialized, true);
        if ($decoded === false) {
            throw new RuntimeException("Invalid base64 PHP-IR payload: {$path}");
        }
        $serialized = $decoded;
    }
    return @unserialize($serialized, ['allowed_classes' => false]);
}

function object_prop(mixed $value, string $name): mixed
{
    if (!is_object($value)) {
        return null;
    }
    foreach ((array) $value as $key => $propertyValue) {
        if ($key === $name || str_ends_with((string) $key, "\0" . $name)) {
            return $propertyValue;
        }
    }
    return null;
}

function resource_references_from_phpir(mixed $phpIr): array
{
    $references = object_prop($phpIr, 'embeddedResourceReferences');
    return is_array($references) ? $references : [];
}

function valid_section_name(string $sectionName): bool
{
    return preg_match('/^\.phpef_resource\.[A-Za-z0-9_.-]+$/', $sectionName) === 1;
}

function resource_assets(array $phpIrFiles): array
{
    $assets = [];
    foreach ($phpIrFiles as $phpIrFile) {
        foreach (resource_references_from_phpir(bundle_payload($phpIrFile)) as $reference) {
            if (!is_array($reference)) {
                continue;
            }
            $path = (string) ($reference['resourcePath'] ?? '');
            $sourceFilePath = (string) ($reference['sourceFilePath'] ?? '');
            $sectionName = (string) ($reference['sectionName'] ?? '');
            $expectedHash = (string) ($reference['sha256'] ?? '');
            $expectedSize = (int) ($reference['size'] ?? -1);
            if ($path === '' || !str_starts_with($path, '/') || $sourceFilePath === '' || $sectionName === '' || !valid_section_name($sectionName)) {
                continue;
            }
            if (!is_file($sourceFilePath) || !is_readable($sourceFilePath)) {
                throw new RuntimeException("Embedded resource source is missing or unreadable: {$sourceFilePath}");
            }
            $actualSize = filesize($sourceFilePath);
            $actualHash = hash_file('sha256', $sourceFilePath);
            if ($actualSize === false || $actualHash === false) {
                throw new RuntimeException("Could not stat embedded resource source: {$sourceFilePath}");
            }
            if ($expectedSize >= 0 && (int) $actualSize !== $expectedSize) {
                throw new RuntimeException("Embedded resource size changed for {$sourceFilePath}");
            }
            if ($expectedHash !== '' && !hash_equals($expectedHash, $actualHash)) {
                throw new RuntimeException("Embedded resource hash changed for {$sourceFilePath}");
            }
            $assets[$path] = [
                'path' => $path,
                'sourceFilePath' => $sourceFilePath,
                'sectionName' => $sectionName,
                'sha256' => $actualHash,
                'size' => (int) $actualSize,
            ];
        }
    }
    ksort($assets);
    return array_values($assets);
}

function pack_le64(int $value): string
{
    $low = $value & 0xffffffff;
    $high = intdiv($value, 0x100000000);
    return pack('V2', $low, $high);
}

function build_binary_resource_index(array $assets): string
{
    $index = "PHPEFRESIDX1\0" . pack('V', count($assets));
    foreach ($assets as $asset) {
        $path = $asset['path'];
        $sectionName = $asset['sectionName'];
        $hash = hex2bin($asset['sha256']);
        if ($hash === false || strlen($hash) !== 32) {
            throw new RuntimeException("Invalid embedded resource sha256 for {$path}");
        }
        if (strlen($path) > 0xffff || strlen($sectionName) > 0xffff) {
            throw new RuntimeException("Embedded resource index field exceeds uint16 limits: {$path}");
        }
        $index .= pack('v', strlen($path)) . $path;
        $index .= pack('v', strlen($sectionName)) . $sectionName;
        $index .= pack_le64((int) $asset['size']);
        $index .= $hash;
    }
    return $index;
}

function build_text_resource_index(array $assets): string
{
    $lines = [
        '# PHPEF embedded resource index debug map',
        '# resource_path' . "\t" . 'section_name' . "\t" . 'size' . "\t" . 'sha256' . "\t" . 'source_file',
    ];
    foreach ($assets as $asset) {
        $lines[] = $asset['path'] . "\t" . $asset['sectionName'] . "\t" . $asset['size'] . "\t" . $asset['sha256'] . "\t" . $asset['sourceFilePath'];
    }
    return implode("\n", $lines) . "\n";
}

function run_process(array $argv): void
{
    $descriptorSpec = [
        0 => ['file', '/dev/null', 'r'],
        1 => ['file', 'php://stdout', 'w'],
        2 => ['file', 'php://stderr', 'w'],
    ];
    $process = proc_open($argv, $descriptorSpec, $pipes);
    if (!is_resource($process)) {
        throw new RuntimeException('Could not start: ' . implode(' ', $argv));
    }
    $status = proc_close($process);
    if ($status !== 0) {
        throw new RuntimeException('Command failed with exit code ' . $status . ': ' . implode(' ', $argv));
    }
}

if ($argc < 6) {
    usage($argv[0]);
    exit(2);
}

$inputElf = $argv[1];
$outputElf = $argv[2];
$indexPath = $argv[3];
$textIndexPath = $argv[4];
$phpIrFiles = array_slice($argv, 5);

try {
    if (!is_file($inputElf)) {
        throw new RuntimeException("Input ELF does not exist: {$inputElf}");
    }
    $assets = resource_assets($phpIrFiles);
    $index = build_binary_resource_index($assets);
    $textIndex = build_text_resource_index($assets);
    $indexDir = dirname($indexPath);
    if ($indexDir !== '' && $indexDir !== '.' && !is_dir($indexDir) && !mkdir($indexDir, 0775, true)) {
        throw new RuntimeException("Could not create resource index directory: {$indexDir}");
    }
    if (file_put_contents($indexPath, $index) === false) {
        throw new RuntimeException("Could not write resource index: {$indexPath}");
    }
    $textIndexDir = dirname($textIndexPath);
    if ($textIndexDir !== '' && $textIndexDir !== '.' && !is_dir($textIndexDir) && !mkdir($textIndexDir, 0775, true)) {
        throw new RuntimeException("Could not create resource debug map directory: {$textIndexDir}");
    }
    if (file_put_contents($textIndexPath, $textIndex) === false) {
        throw new RuntimeException("Could not write resource debug map: {$textIndexPath}");
    }
    if ($assets === []) {
        if (!copy($inputElf, $outputElf)) {
            throw new RuntimeException("Could not copy {$inputElf} to {$outputElf}");
        }
        $inputMode = fileperms($inputElf);
        if ($inputMode !== false) {
            chmod($outputElf, $inputMode & 0777);
        }
        exit(0);
    }

    $notice = "phpefc-add-resource-sections: embedding " . count($assets) . " resource(s) into {$outputElf}; index={$indexPath}; map={$textIndexPath}\n";
    foreach ($assets as $asset) {
        $notice .= "  " . $asset['path'] . " -> " . $asset['sectionName'] . " (" . $asset['size'] . " bytes)\n";
    }
    cli_notice($notice);

    $objcopyArgv = ['objcopy', '--add-section', '.phpef_resource_index=' . $indexPath, '--set-section-flags', '.phpef_resource_index=readonly'];
    $addedSections = [];
    foreach ($assets as $asset) {
        if (isset($addedSections[$asset['sectionName']])) {
            continue;
        }
        $objcopyArgv[] = '--add-section';
        $objcopyArgv[] = $asset['sectionName'] . '=' . $asset['sourceFilePath'];
        $objcopyArgv[] = '--set-section-flags';
        $objcopyArgv[] = $asset['sectionName'] . '=readonly';
        $addedSections[$asset['sectionName']] = true;
    }
    $objcopyArgv[] = $inputElf;
    $objcopyArgv[] = $outputElf;
    run_process($objcopyArgv);
} catch (Throwable $e) {
    cli_error($e->getMessage() . "\n");
    exit(1);
}
