<?php

/**
 * Typing pass for phpfunc_* / RuntimeEmbed call specialisation.
 *
 * This pass rewrites structurally proven generic Zval runtime calls to
 * signature-stable typed RuntimeEmbed fast paths. It relies on
 * Php2ValaFuncResolver candidate metadata instead of spelling-only guesses.
 */
final class ValaIrTypingPhpfuncCallOptimizer
{
    private ?Php2ValaFuncResolver $funcResolver;

    /**
     * Create the optimizer with the default runtime resolver or an injected one for tests.
     *
     * @param Php2ValaFuncResolver|null $funcResolver Resolver to use, or null for default discovery.
     */
    public function __construct(?Php2ValaFuncResolver $funcResolver = null)
    {
        if ($funcResolver !== null) {
            $this->funcResolver = $funcResolver;
            return;
        }

        try {
            $this->funcResolver = Php2ValaFuncResolver::default();
        } catch (Throwable) {
            $this->funcResolver = null;
        }
    }

    /**
     * Apply phpfunc-call typing optimizations.
     *
     * @param object $valaIr ValaIR root node.
     * @return object Optimized ValaIR root node, or the original object when unchanged.
     */
    public function optimize(object $valaIr): object
    {
        if ($this->funcResolver === null) {
            return $valaIr;
        }

        return $this->optimizeNode($valaIr);
    }

    private function optimizeNode(object $node): object
    {
        if ($node instanceof ValaIrProgram) {
            $stmts = $node->stmts();
            $optimizedStmts = $this->optimizeStmtList($stmts);
            return $optimizedStmts === $stmts ? $node : $node->withStmts($optimizedStmts);
        }

        if ($node instanceof ValaIrFunctionDeclaration) {
            $stmts = $node->stmts();
            $optimizedStmts = $this->optimizeStmtList($stmts);
            return $optimizedStmts === $stmts ? $node : $node->withStmts($optimizedStmts);
        }

        if ($node instanceof ValaIrClassDeclaration) {
            $methods = $node->methods();
            $optimizedMethods = $this->optimizeStmtList($methods);
            return $optimizedMethods === $methods ? $node : $node->withMethods($optimizedMethods);
        }

        if ($node instanceof ValaIrCallStatement) {
            return $this->optimizeCallStatement($node);
        }

        if ($node instanceof ValaIrStaticCallExpression) {
            return $this->optimizeStaticCallExpression($node);
        }

        if ($node instanceof ValaIrMethodCallExpression) {
            return $this->optimizeMethodCallExpression($node);
        }

        return $node;
    }

    private function optimizeStmtList(array $stmts): array
    {
        $changed = false;
        $optimized = [];
        foreach ($stmts as $stmt) {
            if (is_object($stmt)) {
                $newStmt = $this->optimizeNode($stmt);
                $changed = $changed || $newStmt !== $stmt;
                $optimized[] = $newStmt;
                continue;
            }
            $optimized[] = $stmt;
        }

        return $changed ? $optimized : $stmts;
    }

    private function optimizeCallStatement(ValaIrCallStatement $call): ValaIrCallStatement
    {
        $replacement = $this->typedFastPathReplacement($call->toArray()['callee'] ?? null, $call->toArray()['args'] ?? null, $call->args());
        if ($replacement !== null) {
            return $call->withCalleeAndArgs($replacement['callee'], $replacement['args']);
        }

        $optimizedArgs = $this->optimizeArgs($call->args());
        return $optimizedArgs === $call->args()
            ? $call
            : $call->withCalleeAndArgs((string) ($call->toArray()['callee'] ?? ''), $optimizedArgs);
    }

    private function optimizeStaticCallExpression(ValaIrStaticCallExpression $call): ValaIrStaticCallExpression
    {
        $replacement = $this->typedFastPathReplacement($call->toArray()['callee'] ?? null, $call->toArray()['args'] ?? null, $call->args());
        if ($replacement !== null) {
            return $call->withCalleeAndArgs($replacement['callee'], $replacement['args']);
        }

        $optimizedArgs = $this->optimizeArgs($call->args());
        return $optimizedArgs === $call->args()
            ? $call
            : $call->withCalleeAndArgs((string) ($call->toArray()['callee'] ?? ''), $optimizedArgs);
    }

    private function optimizeArgs(array $args): array
    {
        $changed = false;
        $optimized = [];
        foreach ($args as $arg) {
            if (is_object($arg)) {
                $newArg = $this->optimizeNode($arg);
                $changed = $changed || $newArg !== $arg;
                $optimized[] = $newArg;
                continue;
            }
            $optimized[] = $arg;
        }

        return $changed ? $optimized : $args;
    }

    private function optimizeMethodCallExpression(ValaIrMethodCallExpression $call): ValaIrMethodCallExpression
    {
        $receiver = $call->receiver();
        $optimizedReceiver = $this->optimizeNode($receiver);
        $optimizedArgs = $this->optimizeArgs($call->args());
        if ($optimizedReceiver === $receiver && $optimizedArgs === $call->args()) {
            return $call;
        }

        return $call->withReceiverAndArgs($optimizedReceiver, $optimizedArgs);
    }

    private function runtimeNameFromCallee(mixed $callee): ?string
    {
        if (!is_string($callee)) {
            return null;
        }
        $parts = explode('.', $callee);
        $runtimeName = end($parts);
        if (!is_string($runtimeName) || preg_match('/^phpfunc_[A-Za-z_][A-Za-z0-9_]*$/', $runtimeName) !== 1) {
            return null;
        }

        return $runtimeName;
    }

    private function baseRuntimeNameFromCallee(mixed $callee): ?string
    {
        $runtimeName = $this->runtimeNameFromCallee($callee);
        if ($runtimeName === null || str_contains($runtimeName, '_typed_')) {
            return null;
        }

        return $runtimeName;
    }

    private function runtimeNamespaceFromCallee(mixed $callee): ?string
    {
        if (!is_string($callee) || $callee === '') {
            return null;
        }
        $lastDot = strrpos($callee, '.');
        if ($lastDot === false) {
            return 'PhpefRuntimeCore';
        }

        return substr($callee, 0, $lastDot);
    }

    private function phpFunctionNameFromBaseRuntimeName(string $baseRuntimeName): ?string
    {
        if (!str_starts_with($baseRuntimeName, 'phpfunc_')) {
            return null;
        }

        $phpFunctionName = substr($baseRuntimeName, strlen('phpfunc_'));
        if ($phpFunctionName === '' || preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $phpFunctionName) !== 1) {
            return null;
        }

        return $phpFunctionName;
    }

    private function analyzeTypedArguments(mixed $debugArgs, array $irArgs): ?array
    {
        if (!is_array($debugArgs) || count($debugArgs) !== count($irArgs)) {
            return null;
        }
        if (count($debugArgs) === 0) {
            return [
                'argumentTypes' => [],
                'replacementArgs' => [],
            ];
        }

        $argumentTypes = [];
        $replacementArgs = [];
        foreach ($debugArgs as $index => $debugArg) {
            if (!is_array($debugArg) || !isset($irArgs[$index]) || !$irArgs[$index] instanceof ValaIrStaticCallExpression) {
                return null;
            }
            $analysis = $this->analyzeTypedArgument($debugArg, $irArgs[$index]);
            if ($analysis === null) {
                return null;
            }
            $argumentTypes[] = $analysis['type'];
            $replacementArgs[] = $analysis['replacement'];
        }

        return [
            'argumentTypes' => $argumentTypes,
            'replacementArgs' => $replacementArgs,
        ];
    }

    private function analyzeTypedArgument(array $debugArg, ValaIrStaticCallExpression $irArg): ?array
    {
        $callee = $debugArg['callee'] ?? null;
        $literalArgs = $irArg->args();
        if (!is_string($callee)) {
            return null;
        }
        $literalAnalysis = count($literalArgs) === 1 ? match ($callee) {
            'Zval.from_string' => $literalArgs[0] instanceof ValaIrStringLiteral ? ['type' => 'string', 'replacement' => $literalArgs[0]] : null,
            'Zval.from_int' => $literalArgs[0] instanceof ValaIrIntLiteral ? ['type' => 'int', 'replacement' => $literalArgs[0]] : null,
            'Zval.from_float' => $literalArgs[0] instanceof ValaIrFloatLiteral ? ['type' => 'double', 'replacement' => $literalArgs[0]] : null,
            'Zval.from_bool' => $literalArgs[0] instanceof ValaIrBoolLiteral ? ['type' => 'bool:' . ((($literalArgs[0]->toArray()['value'] ?? null) === true) ? 'true' : 'false'), 'replacement' => $literalArgs[0]] : null,
            default => null,
        } : null;
        if ($literalAnalysis !== null) {
            return $literalAnalysis;
        }

        return $this->analyzeTypedRuntimeExpression($debugArg, $irArg);
    }

    private function analyzeTypedRuntimeExpression(array $debugArg, ValaIrStaticCallExpression $irArg): ?array
    {
        $baseRuntimeName = $this->baseRuntimeNameFromCallee($debugArg['callee'] ?? null);
        if ($baseRuntimeName === null || $this->funcResolver === null) {
            return null;
        }
        $phpFunctionName = $this->phpFunctionNameFromBaseRuntimeName($baseRuntimeName);
        if ($phpFunctionName === null) {
            return null;
        }
        $argumentAnalysis = $this->analyzeTypedArguments($debugArg['args'] ?? null, $irArg->args());
        if ($argumentAnalysis === null) {
            return null;
        }
        $typedFastPath = $this->funcResolver->resolveTypedFastPath(
            $phpFunctionName,
            $argumentAnalysis['argumentTypes'],
            ['static_php_call', 'native'],
            null,
            $this->runtimeNamespaceFromCallee($debugArg['callee'] ?? null),
        );
        if ($typedFastPath === null) {
            return null;
        }
        $typedSignature = $typedFastPath['typedSignature'] ?? null;
        if (!is_array($typedSignature) || !is_string($typedSignature['returnType'] ?? null)) {
            return null;
        }

        return [
            'type' => $typedSignature['returnType'],
            'replacement' => $irArg->withCalleeAndArgs(
                (string) $typedFastPath['callee'],
                ($typedFastPath['passArguments'] ?? true) ? $argumentAnalysis['replacementArgs'] : [],
            ),
        ];
    }

    private function typedFastPathReplacement(mixed $callee, mixed $debugArgs, array $irArgs): ?array
    {
        $baseRuntimeName = $this->baseRuntimeNameFromCallee($callee);
        if ($baseRuntimeName === null || $this->funcResolver === null) {
            return null;
        }
        $phpFunctionName = $this->phpFunctionNameFromBaseRuntimeName($baseRuntimeName);
        if ($phpFunctionName === null) {
            return null;
        }
        $argumentAnalysis = $this->analyzeTypedArguments($debugArgs, $irArgs);
        if ($argumentAnalysis === null) {
            return null;
        }

        $typedFastPath = $this->funcResolver->resolveTypedFastPath(
            $phpFunctionName,
            $argumentAnalysis['argumentTypes'],
            ['static_php_call', 'native'],
            null,
            $this->runtimeNamespaceFromCallee($callee),
        );
        if ($typedFastPath === null) {
            return null;
        }

        return [
            'callee' => $typedFastPath['callee'],
            'args' => ($typedFastPath['passArguments'] ?? true) ? $argumentAnalysis['replacementArgs'] : [],
        ];
    }

    private function isZvalFromStringLiteral(array $arg): bool
    {
        if (($arg['kind'] ?? null) !== 'StaticCallExpression') {
            return false;
        }
        if (($arg['callee'] ?? null) !== 'Zval.from_string') {
            return false;
        }
        if (!isset($arg['args']) || !is_array($arg['args']) || count($arg['args']) !== 1) {
            return false;
        }

        $literal = $arg['args'][0];
        return is_array($literal) && ($literal['kind'] ?? null) === 'StringLiteral';
    }
}
