<?php

/**
 * Build-time helper for discovering PHP/Zend internal functions from the
 * selected PHP slot and grouping them by extension.
 *
 * The class is intentionally PHP-only for now. pw-build-meson compiles it into
 * pwphpbytecode C so the helper can later be loaded through the embed runtime,
 * while the direct CLI entry point below is used by Meson to create readable
 * debug inventories during the build.
 */
final class PhpefRuntimeEmbedFunctionInventory
{
    private const SPECIAL_ZENDCALL_FUNCTIONS = [
        'Core symbol/existence/metadata helpers observe Zend tables; expose only through explicit zendcall_* source names.' => [
            'extension' => 'Core',
            'functions' => [
                'class_exists',
                'defined',
                'enum_exists',
                'function_exists',
                'get_declared_classes',
                'get_declared_interfaces',
                'get_declared_traits',
                'get_defined_functions',
                'interface_exists',
                'method_exists',
                'property_exists',
                'trait_exists',
            ],
        ],
        'Core callbacks/handlers should be an explicit zendcall_* source-level choice.' => [
            'extension' => 'Core',
            'functions' => [
                'set_error_handler',
                'set_exception_handler',
            ],
        ],
        'Core type/class/object metadata can be ambiguous once PHPEF owns IR/runtime classes too.' => [
            'extension' => 'Core',
            'functions' => [
                'get_class_methods',
                'get_class_vars',
                'get_object_vars',
                'is_a',
                'is_subclass_of',
            ],
        ],
        'Zend request/frame state differs from PHPEF compiled program state.' => [
            'functions' => [
                'debug_backtrace',
                'debug_print_backtrace',
                'func_get_arg',
                'func_get_args',
                'func_num_args',
                'get_defined_vars',
                'get_included_files',
                'get_required_files',
            ],
        ],
        'Needs PHPEF caller-scope/symbol-table intrinsic support.' => [
            'functions' => [
                'compact',
                'extract',
            ],
        ],
        'Mutates or writes by-reference parameters; generic embed call cannot propagate Zend parameter changes back yet.' => [
            'functions' => [
                'array_multisort',
                'array_pop',
                'array_push',
                'array_shift',
                'array_splice',
                'array_unshift',
                'arsort',
                'asort',
                'dns_get_mx',
                'dns_get_record',
                'exec',
                'krsort',
                'ksort',
                'natcasesort',
                'natsort',
                'parse_str',
                'passthru',
                'rsort',
                'shuffle',
                'sort',
                'str_ireplace',
                'str_replace',
                'system',
            ],
        ],
        'Depends on PHP array identity/internal pointer state not modeled by current Zval conversion.' => [
            'functions' => [
                'current',
                'end',
                'key',
                'next',
                'prev',
                'reset',
            ],
        ],
        'Needs PHPEF callable bridge before forwarding to Zend can be faithful.' => [
            'functions' => [
                'array_filter',
                'array_map',
                'array_reduce',
                'array_walk',
                'array_walk_recursive',
                'call_user_func',
                'call_user_func_array',
                'uasort',
                'uksort',
                'usort',
            ],
        ],
    ];

    /**
     * Return internal PHP functions grouped by Reflection extension name.
     *
     */
    public static function collectByExtension(): array
    {
        $functionNames = get_defined_functions()['internal'] ?? [];
        sort($functionNames, SORT_STRING);

        $byExtension = [];
        foreach ($functionNames as $functionName) {
            $reflection = new ReflectionFunction($functionName);
            $extension = $reflection->getExtensionName() ?: '<none>';

            $byExtension[$extension][$functionName] = [
                'name' => $functionName,
                'extension' => $extension,
                'required' => $reflection->getNumberOfRequiredParameters(),
                'total' => $reflection->getNumberOfParameters(),
                'variadic' => self::isVariadic($reflection),
                'return_type' => self::describeReturnType($reflection),
                'parameters' => self::describeParameters($reflection),
            ];
        }

        ksort($byExtension, SORT_STRING);
        foreach ($byExtension as &$functions) {
            ksort($functions, SORT_STRING);
        }
        unset($functions);

        return $byExtension;
    }

    /**
     * Split the function inventory according to Meson-enabled extensions.
     *
     */
    public static function splitByEnabledExtensions(array $enabledExtensions): array
    {
        $byExtension = self::collectByExtension();
        $includeAll = in_array('*', $enabledExtensions, true) || in_array('all', array_map('strtolower', $enabledExtensions), true);
        $enabled = array_fill_keys($enabledExtensions, true);

        $included = [];
        $notIncluded = [];

        foreach ($byExtension as $extension => $functions) {
            if ($includeAll || isset($enabled[$extension])) {
                foreach ($functions as $functionName => $function) {
                    $zendcallReason = self::zendcallReason($function);
                    if ($zendcallReason !== null) {
                        $function['callable_name'] = 'zendcall_' . $function['name'];
                        $function['zendcall_reason'] = $zendcallReason;
                    }
                    $included[$extension][$functionName] = $function;
                }
            } else {
                $notIncluded[$extension] = $functions;
            }
        }

        return [
            'included' => $included,
            'not_included' => $notIncluded,
        ];
    }

    /**
     * Write two readable debug inventories grouped by extension.
     *
     */
    public static function writeDebugFiles(array $enabledExtensions, string $includedPath, string $notIncludedPath): void
    {
        $split = self::splitByEnabledExtensions($enabledExtensions);

        file_put_contents($includedPath, self::formatTextInventory(
            'included PHP functions by extension',
            $enabledExtensions,
            $split['included'],
        ));

        file_put_contents($notIncludedPath, self::formatTextInventory(
            'not included PHP functions by extension',
            $enabledExtensions,
            $split['not_included'],
        ));
    }

    /**
     */
    private static function formatTextInventory(string $title, array $enabledExtensions, array $byExtension): string
    {
        $totalFunctions = 0;
        $typedFunctions = 0;
        foreach ($byExtension as $functions) {
            $totalFunctions += count($functions);
            foreach ($functions as $function) {
                if (self::isTypedFunction($function)) {
                    $typedFunctions++;
                }
            }
        }

        $lines = [
            '# ' . $title,
            '# generated_by=PhpefRuntimeEmbedFunctionInventory',
            '# php_version=' . PHP_VERSION,
            '# generated_via=pwphpbytecode',
            '# inventory_target=phpef-runtime-embed/libphp',
            '# php_sapi=' . PHP_SAPI,
            '# enabled_extensions=' . implode(',', $enabledExtensions),
            '# extension_count=' . count($byExtension),
            '# function_count=' . $totalFunctions,
            '# typed_function_count=' . $typedFunctions . '/' . $totalFunctions,
            '',
        ];

        foreach ($byExtension as $extension => $functions) {
            $functionNameWidth = self::functionNameColumnWidth($functions);
            $lines[] = '[' . $extension . '] count=' . count($functions);
            $lines[] = self::formatFunctionHeaderLine($functionNameWidth);
            $lines[] = self::formatFunctionSeparatorLine($functionNameWidth);
            foreach ($functions as $function) {
                $lines[] = self::formatFunctionLine($function, $functionNameWidth);
            }
            $lines[] = '';
        }

        return implode("\n", $lines);
    }

    /**
     */
    private static function formatFunctionHeaderLine(int $functionNameWidth): string
    {
        return sprintf('%-' . $functionNameWidth . 's  %-40s  %-8s  %-5s  %-8s  %-5s  %-16s  %-40s  %s', 'function', 'wrapper', 'required', 'total', 'variadic', 'typed', 'return', 'params', 'reason');
    }

    /**
     */
    private static function formatFunctionSeparatorLine(int $functionNameWidth): string
    {
        return str_repeat('-', $functionNameWidth) . '  ' . str_repeat('-', 40) . '  ' . str_repeat('-', 8) . '  ' . str_repeat('-', 5) . '  ' . str_repeat('-', 8) . '  ' . str_repeat('-', 5) . '  ' . str_repeat('-', 16) . '  ' . str_repeat('-', 40) . '  ' . str_repeat('-', 40);
    }

    /**
     */
    private static function formatFunctionLine(array $function, int $functionNameWidth): string
    {
        $variadic = $function['variadic'] ? 'yes' : 'no';
        $parameters = [];
        foreach ($function['parameters'] as $parameter) {
            $parameters[] = self::formatParameter($parameter);
        }

        $wrapperName = self::wrapperName($function);
        $displayName = self::displayName($function);

        return sprintf(
            '%-' . $functionNameWidth . 's  %-40s  %-8s  %-5s  %-8s  %-5s  %-16s  %-40s  %s',
            $displayName,
            $wrapperName,
            (string) $function['required'],
            (string) $function['total'],
            $variadic,
            self::isTypedFunction($function) ? 'X' : '',
            ($function['return_type'] ?? '') !== '' ? $function['return_type'] : '-',
            implode('; ', $parameters),
            $function['zendcall_reason'] ?? '',
        );
    }

    private static function isTypedFunction(array $function): bool
    {
        if (($function['variadic'] ?? false)
            || ($function['required'] ?? 0) !== 1
            || ($function['total'] ?? 0) !== 1
            || count($function['parameters'] ?? []) !== 1
            || self::zendcallReason($function) !== null
        ) {
            return false;
        }

        $parameter = $function['parameters'][0];
        if (($parameter['by_reference'] ?? false) || ($parameter['variadic'] ?? false) || ($parameter['allows_null'] ?? false)) {
            return false;
        }

        $argType = self::typedScalarToken($parameter['type'] ?? '');
        $returnType = self::typedScalarToken($function['return_type'] ?? '');

        return $argType === 'string' && in_array($returnType, ['string', 'int'], true);
    }

    private static function typedScalarToken(string $type): ?string
    {
        $type = strtolower(trim($type));
        $type = ltrim($type, '?');
        return match ($type) {
            'string' => 'string',
            'int', 'integer' => 'int',
            'float', 'double' => 'float',
            'bool', 'boolean' => 'bool',
            'void' => 'void',
            default => null,
        };
    }

    private static function functionNameColumnWidth(array $functions): int
    {
        $width = strlen('function');
        foreach ($functions as $function) {
            $width = max($width, strlen(self::displayName($function)));
        }

        return max(52, $width);
    }

    private static function displayName(array $function): string
    {
        $callableName = self::callableName($function);
        if ($callableName !== $function['name']) {
            return $function['name'] . ' (as ' . $callableName . ')';
        }

        return $function['name'];
    }

    private static function callableName(array $function): string
    {
        if (isset($function['callable_name'])) {
            return $function['callable_name'];
        }

        if (self::isSpecialZendcall($function)) {
            return 'zendcall_' . $function['name'];
        }

        return $function['name'];
    }

    private static function wrapperName(array $function): string
    {
        return 'phpfunc_' . self::callableName($function);
    }

    public static function isSpecialZendcallFunction(array $function): bool
    {
        return self::specialZendcallReason($function) !== null;
    }

    private static function isSpecialZendcall(array $function): bool
    {
        return self::isSpecialZendcallFunction($function);
    }

    public static function zendcallReason(array $function): ?string
    {
        $reason = self::specialZendcallReason($function);
        if ($reason !== null) {
            return $reason;
        }

        foreach ($function['parameters'] ?? [] as $parameter) {
            if ($parameter['by_reference'] ?? false) {
                return 'Mutates or writes by-reference parameters; generic embed call cannot propagate Zend parameter changes back yet.';
            }
        }

        foreach ($function['parameters'] ?? [] as $parameter) {
            if (str_contains(strtolower($parameter['type'] ?? ''), 'callable')) {
                return 'Needs PHPEF callable bridge before forwarding to Zend can be faithful.';
            }
        }

        return null;
    }

    private static function specialZendcallReason(array $function): ?string
    {
        $name = $function['name'] ?? '';
        $extension = $function['extension'] ?? '';
        foreach (self::SPECIAL_ZENDCALL_FUNCTIONS as $reason => $group) {
            if (($group['extension'] ?? $extension) !== $extension) {
                continue;
            }

            if (in_array($name, $group['functions'], true)) {
                return $reason;
            }
        }

        return null;
    }

    /**
     */
    private static function describeReturnType(ReflectionFunction $reflection): string
    {
        if (!$reflection->hasReturnType()) {
            return "";
        }

        return (string) $reflection->getReturnType();
    }

    private static function describeParameters(ReflectionFunction $reflection): array
    {
        $parameters = [];
        foreach ($reflection->getParameters() as $parameter) {
            $parameters[] = [
                'name' => $parameter->getName(),
                'type' => $parameter->hasType() ? (string) $parameter->getType() : '',
                'optional' => $parameter->isOptional(),
                'variadic' => $parameter->isVariadic(),
                'by_reference' => $parameter->isPassedByReference(),
                'allows_null' => $parameter->allowsNull(),
            ];
        }

        return $parameters;
    }

    private static function isVariadic(ReflectionFunction $reflection): bool
    {
        foreach ($reflection->getParameters() as $parameter) {
            if ($parameter->isVariadic()) {
                return true;
            }
        }

        return false;
    }

    /**
     */
    private static function formatParameter(array $parameter): string
    {
        $prefix = ($parameter['by_reference'] ?? false ? '&' : '') . ($parameter['variadic'] ? '...' : '');
        $type = $parameter['type'] !== '' ? $parameter['type'] . ' ' : '';
        $optional = $parameter['optional'] ? ' = optional' : '';
        $nullable = $parameter['allows_null'] ? ' nullable' : '';

        return $prefix . $type . '$' . $parameter['name'] . $optional . $nullable;
    }
}

/**
 * Generates the embed PHP function wrapper Vala source from the readable
 * function inventory produced by PhpefRuntimeEmbedFunctionInventory.
 */
final class PhpefRuntimeEmbedFunctionValaGenerator
{
    private const EXCLUDED_FUNCTIONS_BEGIN = '// PHPEF_RUNTIME_EMBED_EXCLUDED_FUNCTIONS_BEGIN v1';
    private const EXCLUDED_FUNCTIONS_END = '// PHPEF_RUNTIME_EMBED_EXCLUDED_FUNCTIONS_END';
    private const VAPI_HEADER_ANNOTATION_TEXT = 'annotated by phpef-runtime-embed function inventory postprocess';

    /**
     * Generate phpef_runtime_embed_functions.vala from functions-included.txt.
     */
    public static function generateFile(string $includedFunctionsPath, string $valaOutputPath): void
    {
        $functions = self::parseIncludedFunctions($includedFunctionsPath);
        $vala = self::generateVala($functions);
        file_put_contents($valaOutputPath, $vala);
    }


    /**
     * Generate installable Markdown documentation from the generated function
     * inventories. The source .txt files remain build artifacts; this document
     * is the packaged human-readable form.
     */
    public static function generateMarkdownFile(string $includedFunctionsPath, string $notIncludedFunctionsPath, string $markdownOutputPath): void
    {
        $included = file_get_contents($includedFunctionsPath);
        if ($included === false) {
            throw new RuntimeException('Could not read included functions inventory: ' . $includedFunctionsPath);
        }

        $notIncluded = file_get_contents($notIncludedFunctionsPath);
        if ($notIncluded === false) {
            throw new RuntimeException('Could not read not-included functions inventory: ' . $notIncludedFunctionsPath);
        }

        $out = [];
        $out[] = '# phpef-runtime-embed PHP function inventory';
        $out[] = '';
        $out[] = 'This document is generated at build time from the selected PHP slot and extension configuration.';
        $out[] = '';
        $out[] = 'Source build artifacts:';
        $out[] = '- `' . basename($includedFunctionsPath) . '`';
        $out[] = '- `' . basename($notIncludedFunctionsPath) . '`';
        $out[] = '';
        $out[] = '## Included functions';
        $out[] = '';
        $out[] = 'These PHP functions are wrapped by phpef-runtime-embed for the configured extension set. If a function is exposed only as an explicit zendcall_* wrapper, the reason column explains why the normal phpfunc_* wrapper is intentionally not implemented.';
        $out[] = '';
        $out[] = '```text';
        $out[] = rtrim($included, "\n");
        $out[] = '```';
        $out[] = '';
        $out[] = '## Not-included extension functions';
        $out[] = '';
        $out[] = 'These PHP functions were discovered in extensions that are not part of the configured extension set. This is separate from zendcall-only functions, which are included and wrapped as explicit zendcall_* escape hatches.';
        $out[] = '';
        $out[] = '```text';
        $out[] = rtrim($notIncluded, "\n");
        $out[] = '```';
        $out[] = '';

        file_put_contents($markdownOutputPath, implode("\n", $out));
    }

    /**
     * Add PHP source metadata comments to the public VAPI generated by valac.
     *
     * valac does not preserve Vala source comments in generated .vapi files, so
     * Meson runs this as a post-processing step after the library target wrote
     * its VAPI. The transformation is idempotent for our generated comments.
     */
    public static function annotateVapiFile(string $includedFunctionsPath, string $notIncludedFunctionsPath, string $vapiPath, string $stampPath): void
    {
        $functions = self::parseIncludedFunctions($includedFunctionsPath);
        $commentsByWrapper = [];
        foreach ($functions as $function) {
            $commentsByWrapper[self::wrapperName($function)] = self::functionMetadataComment($function);
        }
        foreach (self::extraTypedFastPathComments($functions) as $wrapperName => $comment) {
            $commentsByWrapper[$wrapperName] = $comment;
        }

        $excludedFunctionsByExtension = self::parseExcludedFunctionsByExtension($notIncludedFunctionsPath);

        $vapi = file($vapiPath, FILE_IGNORE_NEW_LINES);
        if ($vapi === false) {
            throw new RuntimeException('Could not read VAPI for annotation: ' . $vapiPath);
        }

        $out = [];
        $count = count($vapi);
        $insideExcludedBlock = false;
        for ($i = 0; $i < $count; $i++) {
            $line = $vapi[$i];

            if ($line === self::EXCLUDED_FUNCTIONS_BEGIN) {
                $insideExcludedBlock = true;
                continue;
            }
            if ($insideExcludedBlock) {
                if ($line === self::EXCLUDED_FUNCTIONS_END) {
                    $insideExcludedBlock = false;
                }
                continue;
            }

            if (self::isVapiHeaderAnnotation($line)) {
                continue;
            }

            if (self::isGeneratedVapiMetadataComment($line)) {
                continue;
            }

            if (str_contains($line, ' generated by valac ')) {
                $out[] = $line;
                $out[] = self::formatVapiHeaderAnnotation($line);
                continue;
            }

            if (preg_match('/^\t\[CCode /', $line)
                && isset($vapi[$i + 1])
                && preg_match('/^\tpublic static \S+ (phpfunc_[A-Za-z_][A-Za-z0-9_]*) \(/', $vapi[$i + 1], $matches)
                && isset($commentsByWrapper[$matches[1]])) {
                $out[] = "\t" . $commentsByWrapper[$matches[1]];
                $out[] = $line;
                continue;
            }

            if (preg_match('/^\tpublic static \S+ (phpfunc_[A-Za-z_][A-Za-z0-9_]*) \(/', $line, $matches)
                && isset($commentsByWrapper[$matches[1]])
                && (count($out) === 0 || !str_starts_with($out[count($out) - 1], "\t[CCode "))) {
                $out[] = "\t" . $commentsByWrapper[$matches[1]];
            }

            $out[] = $line;
        }

        while (count($out) > 0 && $out[count($out) - 1] === '') {
            array_pop($out);
        }

        $out[] = '';
        array_push($out, ...self::excludedFunctionsCommentBlock($excludedFunctionsByExtension));

        $content = implode("\n", $out) . "\n";
        file_put_contents($vapiPath, $content);
        file_put_contents($stampPath, 'annotated ' . $vapiPath . "\n");
    }

    /**
     * Metadata comments for typed fast paths outside the generic Zval bridge.
     */
    private static function extraTypedFastPathComments(array $functions): array
    {
        $comments = [
            'phpfunc_echo_typed_void_arg1_string' => '// PHPEF typed_fast_path native; source language_construct; PHP construct: echo; typed_signature _typed_void_arg1_string',
            'phpfunc_print_typed_int_arg1_string' => '// PHPEF typed_fast_path native; source language_construct; PHP construct: print; typed_signature _typed_int_arg1_string',
        ];

        foreach ($functions as $function) {
            $typed = self::typedDynamicCallWrapper($function);
            if ($typed === null) {
                continue;
            }
            $comments[$typed['wrapper_name']] = '// PHPEF typed_fast_path static_php_call; source php_function; PHP function: '
                . self::sanitizeCommentPart($function['name'] ?? 'unknown')
                . '; typed_signature ' . $typed['signature_suffix']
                . '; boundary ' . $typed['boundary'];
        }

        return $comments;
    }

    /**
     * Detect comments owned by this postprocess step so repeated annotation is
     * idempotent for both generated PHP metadata and native fast-path metadata.
     */
    private static function isGeneratedVapiMetadataComment(string $line): bool
    {
        return preg_match('/^\t\/\/ PHP /', $line) === 1
            || preg_match('/^\t\/\/ PHPEF typed_fast_path /', $line) === 1;
    }

    /**
     * Render the postprocess marker using the same one-line comment style as valac's VAPI header.
     */
    private static function formatVapiHeaderAnnotation(string $valacGeneratedLine): string
    {
        if (preg_match('/^(\s*)\/\*.*\*\/(\s*)$/', $valacGeneratedLine, $matches)) {
            return $matches[1] . '/* ' . self::VAPI_HEADER_ANNOTATION_TEXT . ' */' . $matches[2];
        }

        if (preg_match('/^(\s*)\/\//', $valacGeneratedLine, $matches)) {
            return $matches[1] . '// ' . self::VAPI_HEADER_ANNOTATION_TEXT;
        }

        return '// ' . self::VAPI_HEADER_ANNOTATION_TEXT;
    }

    /**
     * Detect any previous rendering of the postprocess marker for idempotency.
     */
    private static function isVapiHeaderAnnotation(string $line): bool
    {
        return str_contains($line, self::VAPI_HEADER_ANNOTATION_TEXT);
    }

    /**
     * Parse the not-included text inventory into a stable extension => function[] map.
     */
    private static function parseExcludedFunctionsByExtension(string $path): array
    {
        $lines = file($path, FILE_IGNORE_NEW_LINES);
        if ($lines === false) {
            throw new RuntimeException('Could not read not-included functions inventory: ' . $path);
        }

        $byExtension = [];
        $currentExtension = null;
        $wrapperColumn = null;

        foreach ($lines as $line) {
            if ($line === '' || $line[0] === '#') {
                continue;
            }

            if ($line[0] === '[') {
                if (preg_match('/^\[([^\]]+)\]/', $line, $matches)) {
                    $currentExtension = $matches[1];
                    $byExtension[$currentExtension] ??= [];
                }
                $wrapperColumn = null;
                continue;
            }

            if (str_starts_with($line, 'function') && str_contains($line, 'wrapper')) {
                $wrapperColumn = strpos($line, 'wrapper');
                continue;
            }

            if ($currentExtension === null || str_starts_with($line, '-----') || $wrapperColumn === null) {
                continue;
            }

            if (strlen($line) <= $wrapperColumn) {
                continue;
            }

            $displayName = rtrim(substr($line, 0, $wrapperColumn));
            if ($displayName === '') {
                continue;
            }

            if (preg_match('/^(.*?)\s+\(as\s+[^)]+\)$/', $displayName, $matches)) {
                $displayName = $matches[1];
            }

            $byExtension[$currentExtension][$displayName] = true;
        }

        foreach ($byExtension as $extension => $functions) {
            $names = array_keys($functions);
            sort($names, SORT_STRING);
            $byExtension[$extension] = $names;
        }
        ksort($byExtension, SORT_STRING);

        return $byExtension;
    }

    /**
     * Build the machine-readable VAPI comment block for excluded PHP functions.
     */
    private static function excludedFunctionsCommentBlock(array $excludedFunctionsByExtension): array
    {
        $lines = [self::EXCLUDED_FUNCTIONS_BEGIN];

        foreach ($excludedFunctionsByExtension as $extension => $functions) {
            $json = json_encode(
                [
                    'extension' => $extension,
                    'functions' => array_values($functions),
                ],
                JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
            );
            if ($json === false) {
                throw new RuntimeException('Could not encode excluded functions metadata as JSON');
            }
            $lines[] = '// ' . $json;
        }

        $lines[] = self::EXCLUDED_FUNCTIONS_END;

        return $lines;
    }

    /**
     */
    private static function parseIncludedFunctions(string $path): array
    {
        $lines = file($path, FILE_IGNORE_NEW_LINES);
        if ($lines === false) {
            throw new RuntimeException('Could not read included functions inventory: ' . $path);
        }

        $functions = [];
        $currentExtension = 'unknown';
        foreach ($lines as $line) {
            if ($line === '' || $line[0] === '#') {
                continue;
            }
            if ($line[0] === '[') {
                if (preg_match('/^\[([^\]]+)\]/', $line, $matches)) {
                    $currentExtension = $matches[1];
                }
                continue;
            }
            if (str_starts_with($line, 'function ') || str_starts_with($line, '-----')) {
                continue;
            }

            $matches = [];
            if (preg_match("/^([A-Za-z_][A-Za-z0-9_]*)(?:\s+\(as\s+([A-Za-z_][A-Za-z0-9_]*)\))?\s+(phpfunc_[A-Za-z_][A-Za-z0-9_]*)\s+(\d+)\s+(\d+)\s+(yes|no)\s+(X?)\s+(\S+)\s*(.*)$/", $line, $matches)) {
                $parameters = self::parseParameters($matches[9]);
                $functions[] = [
                    'extension' => $currentExtension,
                    'name' => $matches[1],
                    'callable_name' => ($matches[2] ?? '') !== '' ? $matches[2] : $matches[1],
                    'wrapper_name' => $matches[3],
                    'required' => (int) $matches[4],
                    'total' => (int) $matches[5],
                    'variadic' => $matches[6] === 'yes',
                    'typed' => $matches[7] === 'X',
                    'return_type' => $matches[8] !== '-' ? $matches[8] : '',
                    'parameters' => $parameters,
                ];
                continue;
            }

            if (preg_match('/^([A-Za-z_][A-Za-z0-9_]*)\s+(\d+)\s+(\d+)\s+(yes|no)\s+(.*)$/', $line, $matches)) {
                $parameters = self::parseParameters($matches[5]);
                $functions[] = [
                    'extension' => $currentExtension,
                    'name' => $matches[1],
                    'required' => (int) $matches[2],
                    'total' => (int) $matches[3],
                    'variadic' => $matches[4] === 'yes',
                    'parameters' => $parameters,
                ];
                continue;
            }
        }

        usort($functions, static fn (array $a, array $b): int => strcmp($a['name'], $b['name']));

        return $functions;
    }

    /**
     */
    private static function parseParameters(string $parameterText): array
    {
        $parameters = [];
        $usedNames = [];
        $parts = array_map('trim', explode(';', $parameterText));

        foreach ($parts as $index => $part) {
            if ($part === '') {
                continue;
            }

            if (!preg_match('/^(&)?(\.\.\.)?\s*(.*?)\$([A-Za-z_][A-Za-z0-9_]*)(?:\s|$)/', $part, $matches)) {
                continue;
            }

            $name = self::sanitizeValaIdentifier($matches[4], $index, $usedNames);
            $usedNames[$name] = true;

            $parameters[] = [
                'name' => $name,
                'type' => trim($matches[3]),
                'variadic' => ($matches[2] ?? '') === '...',
                'by_reference' => ($matches[1] ?? '') === '&',
                'allows_null' => str_contains($part, ' nullable'),
            ];
        }

        return $parameters;
    }

    /**
     */
    private static function sanitizeValaIdentifier(string $name, int $index, array $usedNames): string
    {
        $name = preg_replace('/[^A-Za-z0-9_]/', '_', $name) ?? '';
        if ($name === '' || preg_match('/^[0-9]/', $name)) {
            $name = 'arg' . $index;
        }

        $reserved = [
            'abstract' => true, 'as' => true, 'async' => true, 'base' => true, 'bool' => true,
            'break' => true, 'case' => true, 'catch' => true, 'char' => true, 'class' => true,
            'const' => true, 'construct' => true, 'continue' => true, 'default' => true,
            'delegate' => true, 'delete' => true, 'do' => true, 'double' => true, 'dynamic' => true,
            'else' => true, 'ensures' => true, 'enum' => true, 'errordomain' => true,
            'extern' => true, 'false' => true, 'finally' => true, 'float' => true, 'for' => true,
            'foreach' => true, 'get' => true, 'if' => true, 'in' => true, 'inline' => true,
            'int' => true, 'interface' => true, 'internal' => true, 'is' => true, 'lock' => true,
            'long' => true, 'namespace' => true, 'new' => true, 'null' => true, 'out' => true,
            'override' => true, 'owned' => true, 'private' => true, 'protected' => true,
            'public' => true, 'ref' => true, 'requires' => true, 'return' => true, 'set' => true,
            'short' => true, 'signal' => true, 'sizeof' => true, 'static' => true, 'string' => true,
            'struct' => true, 'switch' => true, 'this' => true, 'throw' => true, 'throws' => true,
            'true' => true, 'try' => true, 'typeof' => true, 'uint' => true, 'ulong' => true,
            'unowned' => true, 'ushort' => true, 'using' => true, 'var' => true, 'virtual' => true,
            'void' => true, 'volatile' => true, 'weak' => true, 'while' => true, 'yield' => true,
        ];

        if (isset($reserved[$name])) {
            $name .= '_arg';
        }

        $baseName = $name;
        $suffix = 2;
        while (isset($usedNames[$name])) {
            $name = $baseName . '_' . $suffix;
            $suffix++;
        }

        return $name;
    }

    /**
     */
    private static function generateVala(array $functions): string
    {
        $out = [];
        $out[] = '// This file is generated by phpef-runtime-embed-function-vala-generator.';
        $out[] = '// Do not edit manually; change src/phphelpers/phpef_runtime_embed_function_vala_generator.php instead.';
        $out[] = '';
        $out[] = 'namespace PhpefRuntimeEmbed {';
        $out[] = '    [CCode (cname = "phpef_runtime_embed_call_php_function")]';
        $out[] = '    private extern PhpefRuntimeCore.Zval call_php_function(string function_name, PhpefRuntimeCore.Zval[] args);';
        $out[] = '    [CCode (cname = "phpef_runtime_embed_call_php_function_typed_string_arg1_string")]';
        $out[] = '    private extern string call_php_function_typed_string_arg1_string(string function_name, string arg1);';
        $out[] = '    [CCode (cname = "phpef_runtime_embed_call_php_function_typed_int_arg1_string")]';
        $out[] = '    private extern int64 call_php_function_typed_int_arg1_string(string function_name, string arg1);';
        $out[] = '';

        foreach ($functions as $function) {
            $out[] = self::generateFunctionWrapper($function);
        }

        $out[] = '}';
        $out[] = '';

        return implode("\n", $out);
    }

    /**
     */
    private static function generateFunctionWrapper(array $function): string
    {
        $name = $function['name'];
        $required = $function['required'];
        $variadic = $function['variadic'];
        $allParameters = $function['parameters'];
        $fixedParameters = [];
        $variadicName = 'varargs';

        foreach ($allParameters as $index => $parameter) {
            if ($parameter['variadic']) {
                $variadicName = $parameter['name'];
                continue;
            }
            $fixedParameters[] = $parameter['name'];
        }

        $fixedCount = count($fixedParameters);
        if ($fixedCount === 0 && !$variadic && $function['total'] > 0) {
            for ($i = 0; $i < $function['total']; $i++) {
                $fixedParameters[] = self::fallbackArgName($i);
            }
            $fixedCount = count($fixedParameters);
        }

        $params = [];
        for ($i = 0; $i < $fixedCount; $i++) {
            $argName = $fixedParameters[$i];
            if (!$variadic && $i >= $required) {
                $params[] = 'PhpefRuntimeCore.Zval? ' . $argName . ' = null';
            } else {
                $params[] = 'PhpefRuntimeCore.Zval ' . $argName;
            }
        }
        if ($variadic) {
            $params[] = 'params PhpefRuntimeCore.Zval[] ' . $variadicName;
        }

        $wrapperName = self::wrapperName($function);

        $lines = [];
        $lines[] = '    ' . self::functionMetadataComment($function);
        $lines[] = '    public PhpefRuntimeCore.Zval ' . $wrapperName . '(' . implode(', ', $params) . ') {';

        if ($variadic) {
            if ($fixedCount > 0) {
                $lines[] = '        PhpefRuntimeCore.Zval[] call_args = { ' . implode(', ', $fixedParameters) . ' };';
            } else {
                $lines[] = '        PhpefRuntimeCore.Zval[] call_args = {};';
            }
            $lines[] = '        foreach (var vararg in ' . $variadicName . ') {';
            $lines[] = '            call_args += vararg;';
            $lines[] = '        }';
            $lines[] = '        return call_php_function("' . $name . '", call_args);';
            $lines[] = '    }';
            $lines[] = '';
            return implode("\n", $lines);
        }

        if ($fixedCount === 0) {
            $lines[] = '        PhpefRuntimeCore.Zval[] call_args = {};';
            $lines[] = '        return call_php_function("' . $name . '", call_args);';
            $lines[] = '    }';
            $lines[] = '';
            return implode("\n", $lines);
        }

        for ($count = $required; $count < $fixedCount; $count++) {
            $optionalArg = $fixedParameters[$count];
            $args = array_slice($fixedParameters, 0, $count);
            $lines[] = '        if (' . $optionalArg . ' == null) {';
            $lines[] = '            PhpefRuntimeCore.Zval[] call_args = { ' . implode(', ', $args) . ' };';
            $lines[] = '            return call_php_function("' . $name . '", call_args);';
            $lines[] = '        }';
            $lines[] = '';
        }

        $lines[] = '        PhpefRuntimeCore.Zval[] call_args = { ' . implode(', ', $fixedParameters) . ' };';
        $lines[] = '        return call_php_function("' . $name . '", call_args);';
        $lines[] = '    }';
        $lines[] = '';

        $typedWrapper = self::generateTypedDynamicCallWrapper($function);
        if ($typedWrapper !== null) {
            $lines[] = $typedWrapper;
        }

        return implode("\n", $lines);
    }

    private static function generateTypedDynamicCallWrapper(array $function): ?string
    {
        $typed = self::typedDynamicCallWrapper($function);
        if ($typed === null) {
            return null;
        }

        $parameter = $function['parameters'][0];
        $argName = $parameter['name'];
        $returnType = $typed['vala_return_type'];
        $wrapperName = $typed['wrapper_name'];
        $boundary = $typed['boundary'];

        return '    // PHPEF typed_fast_path static_php_call; source php_function; PHP function: '
            . self::sanitizeCommentPart($function['name'] ?? 'unknown')
            . '; typed_signature ' . $typed['signature_suffix']
            . '; boundary ' . $boundary . "\n"
            . '    public ' . $returnType . ' ' . $wrapperName . '(string ' . $argName . ') {' . "\n"
            . '        return ' . $boundary . '("' . $function['name'] . '", ' . $argName . ');' . "\n"
            . '    }' . "\n";
    }

    private static function typedDynamicCallWrapper(array $function): ?array
    {
        if (($function['variadic'] ?? false)
            || ($function['required'] ?? 0) !== 1
            || ($function['total'] ?? 0) !== 1
            || count($function['parameters'] ?? []) !== 1
            || PhpefRuntimeEmbedFunctionInventory::zendcallReason($function) !== null
        ) {
            return null;
        }

        $parameter = $function['parameters'][0];
        if (($parameter['by_reference'] ?? false) || ($parameter['variadic'] ?? false) || ($parameter['allows_null'] ?? false)) {
            return null;
        }

        $argType = self::typedScalarToken($parameter['type'] ?? '');
        $returnType = self::typedScalarToken($function['return_type'] ?? '');
        if ($argType !== 'string' || !in_array($returnType, ['string', 'int'], true)) {
            return null;
        }

        $signature = '_typed_' . $returnType . '_arg1_' . $argType;
        return [
            'wrapper_name' => self::wrapperName($function) . $signature,
            'signature_suffix' => $signature,
            'boundary' => 'call_php_function_typed_' . $returnType . '_arg1_' . $argType,
            'vala_return_type' => $returnType === 'int' ? 'int64' : 'string',
        ];
    }

    private static function typedScalarToken(string $type): ?string
    {
        $type = strtolower(trim($type));
        $type = ltrim($type, '?');
        return match ($type) {
            'string' => 'string',
            'int', 'integer' => 'int',
            'float', 'double' => 'float',
            'bool', 'boolean' => 'bool',
            'void' => 'void',
            default => null,
        };
    }

    private static function wrapperName(array $function): string
    {
        if (self::isSpecialZendcall($function)) {
            return 'phpfunc_' . self::callableName($function);
        }

        return $function['wrapper_name'] ?? ('phpfunc_' . $function['name']);
    }

    private static function functionMetadataComment(array $function): string
    {
        $extension = self::sanitizeCommentPart($function['extension'] ?? 'unknown');
        $name = self::sanitizeCommentPart($function['name'] ?? 'unknown');
        $callableName = self::sanitizeCommentPart(self::callableName($function));
        $wrapperName = self::sanitizeCommentPart(self::wrapperName($function));
        $zendcallReason = PhpefRuntimeEmbedFunctionInventory::zendcallReason($function);
        $zendcallReasonText = $zendcallReason !== null ? '; zendcall_reason: ' . self::sanitizeCommentPart($zendcallReason) : '';

        if ($callableName !== $name) {
            return '// PHP ' . $extension . ': ' . $name . ' (as ' . $callableName . '); wrapper ' . $wrapperName . $zendcallReasonText;
        }

        return '// PHP ' . $extension . ': ' . $name . $zendcallReasonText;
    }

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

    private static function isSpecialZendcall(array $function): bool
    {
        return PhpefRuntimeEmbedFunctionInventory::isSpecialZendcallFunction($function);
    }


    private static function callableName(array $function): string
    {
        return $function['callable_name'] ?? ('zendcall_' . $function['name']);
    }


    private static function fallbackArgName(int $index): string
    {
        return 'arg' . $index;
    }
}

$mode = $argv[1] ?? '';


if ($mode === '--generate-markdown') {
    $includedFunctionsPath = $argv[2] ?? '';
    $notIncludedFunctionsPath = $argv[3] ?? '';
    $markdownOutputPath = $argv[4] ?? '';

    if ($includedFunctionsPath === '' || $notIncludedFunctionsPath === '' || $markdownOutputPath === '') {
        file_put_contents('php://stderr', "usage: phpef-runtime-embed-function-inventory --generate-markdown FUNCTIONS_INCLUDED_TXT FUNCTIONS_NOT_INCLUDED_TXT MARKDOWN_OUT\n");
        exit(2);
    }

    PhpefRuntimeEmbedFunctionValaGenerator::generateMarkdownFile($includedFunctionsPath, $notIncludedFunctionsPath, $markdownOutputPath);
    exit(0);
}

if ($mode === '--annotate-vapi') {
    $includedFunctionsPath = $argv[2] ?? '';
    $notIncludedFunctionsPath = $argv[3] ?? '';
    $vapiPath = $argv[4] ?? '';
    $stampPath = $argv[5] ?? '';

    if ($includedFunctionsPath === '' || $notIncludedFunctionsPath === '' || $vapiPath === '' || $stampPath === '') {
        file_put_contents('php://stderr', "usage: phpef-runtime-embed-function-inventory --annotate-vapi FUNCTIONS_INCLUDED_TXT FUNCTIONS_NOT_INCLUDED_TXT VAPI_PATH STAMP_OUT\n");
        exit(2);
    }

    PhpefRuntimeEmbedFunctionValaGenerator::annotateVapiFile($includedFunctionsPath, $notIncludedFunctionsPath, $vapiPath, $stampPath);
    exit(0);
}

if ($mode === '--generate-vala') {
    $includedFunctionsPath = $argv[2] ?? '';
    $valaOutputPath = $argv[3] ?? '';

    if ($includedFunctionsPath === '' || $valaOutputPath === '') {
        file_put_contents('php://stderr', "usage: phpef-runtime-embed-function-inventory --generate-vala FUNCTIONS_INCLUDED_TXT VALA_OUT\n");
        exit(2);
    }

    PhpefRuntimeEmbedFunctionValaGenerator::generateFile($includedFunctionsPath, $valaOutputPath);
    exit(0);
}

$enabledCsv = $argv[1] ?? '';
$includedPath = $argv[2] ?? '';
$notIncludedPath = $argv[3] ?? '';

if ($includedPath === '' || $notIncludedPath === '') {
    file_put_contents('php://stderr', "usage: phpef-runtime-embed-function-inventory EXT1,EXT2 INCLUDED_OUT NOT_INCLUDED_OUT\n");
    exit(2);
}

$enabledExtensions = array_values(array_filter(
    array_map('trim', explode(',', $enabledCsv)),
    static fn (string $value): bool => $value !== '',
));

PhpefRuntimeEmbedFunctionInventory::writeDebugFiles($enabledExtensions, $includedPath, $notIncludedPath);
