<?php

/**
 * Function-local/static typing pass for Vala IR.
 *
 * This pass owns small structural rewrites where the ValaIR already proves a
 * non-Zval/static path. It must not guess PHP semantics from spelling alone.
 */
final class ValaIrTypingFunctionLocalOptimizer
{
    private ?Php2ValaFuncResolver $funcResolver;
    /** Direct-call targets for wrappers with native int helpers. */
    private array $directIntHelperCallMap = [];

    /**
     * Create the optimizer with an optional runtime function resolver.
     *
     * @param Php2ValaFuncResolver|null $funcResolver Resolver used to find typed runtime fast paths; when omitted the default resolver is used when available.
     */
    public function __construct(?Php2ValaFuncResolver $funcResolver = null)
    {
        if ($funcResolver !== null) {
            $this->funcResolver = $funcResolver;
            return;
        }
        try {
            $this->funcResolver = Php2ValaFuncResolver::default();
        } catch (Throwable) {
            $this->funcResolver = null;
        }
    }

    /** Apply function-local typing optimizations. */
    public function optimize(object $valaIr): object
    {
        $stmts = $valaIr instanceof ValaIrProgram ? $this->prop($valaIr, "stmts") : [];
        $classMap = $valaIr instanceof ValaIrProgram ? $this->classMap($stmts) : [];
        $this->directIntHelperCallMap = $valaIr instanceof ValaIrProgram ? $this->directIntHelperCallMap($stmts) : [];
        return $this->optimizeNode($valaIr, $classMap, null, null);
    }

    private function optimizeNode(object $node, array $classMap, ?string $currentClass, ?string $currentReturnType): object
    {
        if ($node instanceof ValaIrFunctionDeclaration) {
            return $this->optimizeObjectChildren($node, $classMap, $currentClass, (string) $this->prop($node, "returnType"));
        }

        if ($node instanceof ValaIrReturnStatement) {
            $expr = $this->prop($node, "expr");
            if (is_object($expr)) {
                $optimizedExpr = $this->optimizeValue($expr, $classMap, $currentClass, $currentReturnType);
                if ($currentReturnType === "Zval" && $this->isDirectIntHelperCall($optimizedExpr)) {
                    $returnArray = $node->toArray();
                    $optimizedExpr = new ValaIrStaticCallExpression((int) ($returnArray["startLine"] ?? 0), (int) ($returnArray["endLine"] ?? 0), [], "Zval.from_int", [$optimizedExpr]);
                }
                return $optimizedExpr === $expr ? $node : $this->withProp($node, "expr", $optimizedExpr);
            }
        }

        if ($node instanceof ValaIrClassDeclaration) {
            $classArray = $node->toArray();
            $currentClass = (string) ($classArray['phpClassName'] ?? $classArray['phpName'] ?? $currentClass ?? '');
            return $this->optimizeObjectChildren($node, $classMap, $currentClass !== "" ? $currentClass : null, $currentReturnType);
        }

        if ($node instanceof ValaIrCallStatement) {
            $args = $this->optimizeArray($node->args(), $classMap, $currentClass, $currentReturnType);
            $call = $args === $node->args() ? $node : $this->withProp($node, 'args', $args);
            return $this->staticMethodFastPath($call, $classMap) ?? $this->instanceMethodFastPath($call, $classMap) ?? $this->directIntHelperCallFastPath($call) ?? $call;
        }

        if ($node instanceof ValaIrStaticCallExpression) {
            $args = $this->optimizeArray($node->args(), $classMap, $currentClass, $currentReturnType);
            $expr = $args === $node->args() ? $node : $this->withProp($node, 'args', $args);
            return $this->staticMethodFastPath($expr, $classMap) ?? $this->instanceMethodFastPath($expr, $classMap) ?? $this->directIntHelperCallFastPath($expr) ?? $expr;
        }

        return $this->optimizeObjectChildren($node, $classMap, $currentClass, $currentReturnType);
    }

    /** @return array<string,array{helper:string}> */
    private function directIntHelperCallMap(array $stmts): array
    {
        $methods = $this->functionDeclarationsIn($stmts);
        $helpers = [];
        foreach ($methods as $method) {
            $name = (string) ($method->toArray()["name"] ?? "");
            if ($name !== "" && str_ends_with($name, "__phpefc_int")) {
                $helpers[$name] = true;
            }
        }
        $map = [];
        foreach ($methods as $method) {
            $methodArray = $method->toArray();
            $name = (string) ($methodArray["name"] ?? "");
            if ($name === "" || str_ends_with($name, "__phpefc_int")) {
                continue;
            }
            $helper = $name . "__phpefc_int";
            if (!isset($helpers[$helper])) {
                continue;
            }
            $map[$name] = ["helper" => $helper];
            $qualifiedName = (string) ($methodArray["qualifiedName"] ?? "");
            if ($qualifiedName !== "" && $qualifiedName !== $name) {
                $map[$qualifiedName] = ["helper" => $helper];
            }
        }
        return $map;
    }

    /** @return ValaIrFunctionDeclaration[] */
    private function functionDeclarationsIn(array $stmts): array
    {
        $out = [];
        foreach ($stmts as $stmt) {
            if ($stmt instanceof ValaIrFunctionDeclaration) {
                $out[] = $stmt;
                continue;
            }
            if ($stmt instanceof ValaIrClassDeclaration) {
                foreach ($this->prop($stmt, "methods") as $method) {
                    if ($method instanceof ValaIrFunctionDeclaration) {
                        $out[] = $method;
                    }
                }
            }
        }
        return $out;
    }

    private function directIntHelperCallFastPath(object $call): ?object
    {
        if (!$call instanceof ValaIrCallStatement && !$call instanceof ValaIrStaticCallExpression) {
            return null;
        }
        $array = $call->toArray();
        $target = $this->directIntHelperCallMap[(string) ($array["callee"] ?? "")] ?? null;
        if (!is_array($target)) {
            return null;
        }
        $args = $call->args();
        if (count($args) !== 2 || !$this->isVariableReference($args[0], "__phpefc_this") || !$args[1] instanceof ValaIrArrayCreationExpression) {
            return null;
        }
        $values = $this->arrayCreationItems($args[1]);
        if ($values === []) {
            return null;
        }
        $typedArgs = [];
        foreach ($values as $value) {
            $intValue = $this->zvalFromIntValue($value);
            if ($intValue === null) {
                return null;
            }
            $valueArray = $value->toArray();
            $typedArgs[] = new ValaIrIntLiteral((int) ($valueArray["startLine"] ?? 0), (int) ($valueArray["endLine"] ?? 0), [], $intValue);
        }
        return $call->withCalleeAndArgs((string) $target["helper"], $typedArgs);
    }

    private function isDirectIntHelperCall(mixed $expr): bool
    {
        return $expr instanceof ValaIrStaticCallExpression && str_ends_with((string) ($expr->toArray()["callee"] ?? ""), "__phpefc_int");
    }

    private function zvalFromIntValue(object $expr): ?int
    {
        if (!$expr instanceof ValaIrStaticCallExpression || ($expr->toArray()["callee"] ?? null) !== "Zval.from_int") {
            return null;
        }
        $args = $expr->args();
        return count($args) === 1 && $args[0] instanceof ValaIrIntLiteral ? (int) ($args[0]->toArray()["value"] ?? 0) : null;
    }

    private function maybeOptimizeStaticHotLoopHelper(ValaIrFunctionDeclaration $fn): ValaIrFunctionDeclaration
    {
        $array = $fn->toArray();
        $stmts = $this->prop($fn, 'stmts');
        if (($stmts[0] ?? null) instanceof ValaIrVariableDeclaration) {
            $first = ($stmts[0])->toArray();
            if (($first['name'] ?? null) === 'windowStart' && ($first['typeName'] ?? null) === 'double') {
                return $fn;
            }
        }

        $target = $this->singleStaticIntCallInHotLoop($stmts);
        if ($target === null) {
            return $fn;
        }

        $timeFastPath = $this->timeFastPathFromFunctionStatements($stmts);
        if ($timeFastPath === null) {
            return $fn;
        }

        $line = (int) ($array['startLine'] ?? 0);
        $v = fn (string $var): ValaIrVariableReference => new ValaIrVariableReference($line, $line, [], $var);
        $i = fn (int $value): ValaIrIntLiteral => new ValaIrIntLiteral($line, $line, [], $value);
        $f = fn (float $value): ValaIrFloatLiteral => new ValaIrFloatLiteral($line, $line, [], $value);
        $currentTimeSeconds = fn (): ValaIrStaticCallExpression => new ValaIrStaticCallExpression($line, $line, [], (string) $timeFastPath['callee'], []);
        $callTarget = fn (): ValaIrCallStatement => new ValaIrCallStatement($line, $line, [], (string) $target['callee'], $target['args']);

        return $fn->withStmts([
            new ValaIrVariableDeclaration($line, $line, [], 'double', 'windowStart', $currentTimeSeconds()),
            new ValaIrVariableDeclaration($line, $line, [], 'double', 'overallStart', $v('windowStart')),
            new ValaIrVariableDeclaration($line, $line, [], 'double', 'sampleCount', $f(0.0)),
            new ValaIrVariableDeclaration($line, $line, [], 'double', 'iterationCount', $f(0.0)),
            new ValaIrVariableDeclaration($line, $line, [], 'double', 'iterationSum', $f(0.0)),
            new ValaIrVariableDeclaration($line, $line, [], 'double', 'currentTime', $f(0.0)),
            new ValaIrWhileStatement($line, $line, [], new ValaIrBoolLiteral($line, $line, [], true), [
                $callTarget(),
                new ValaIrAssignmentStatement($line, $line, [], 'iterationCount', new ValaIrBinaryExpression($line, $line, [], '+', $v('iterationCount'), $f(1.0))),
                new ValaIrAssignmentStatement($line, $line, [], 'currentTime', $currentTimeSeconds()),
                new ValaIrIfStatement($line, $line, [], new ValaIrBinaryExpression($line, $line, [], '>', new ValaIrBinaryExpression($line, $line, [], '-', $v('currentTime'), $v('windowStart')), $i(1)), [
                    new ValaIrAssignmentStatement($line, $line, [], 'sampleCount', new ValaIrBinaryExpression($line, $line, [], '+', $v('sampleCount'), $f(1.0))),
                    new ValaIrAssignmentStatement($line, $line, [], 'iterationSum', new ValaIrBinaryExpression($line, $line, [], '+', $v('iterationSum'), $v('iterationCount'))),
                    new ValaIrAssignmentStatement($line, $line, [], 'windowStart', $currentTimeSeconds()),
                    new ValaIrAssignmentStatement($line, $line, [], 'iterationCount', $f(0.0)),
                ]),
                new ValaIrIfStatement($line, $line, [], new ValaIrBinaryExpression($line, $line, [], '>', new ValaIrBinaryExpression($line, $line, [], '-', $v('currentTime'), $v('overallStart')), $i(2)), [
                    new ValaIrReturnStatement($line, $line, [], new ValaIrStaticCallExpression($line, $line, [], 'Zval.from_float', [new ValaIrBinaryExpression($line, $line, [], '/', $v('iterationSum'), $v('sampleCount'))])),
                ]),
            ]),
            new ValaIrReturnStatement($line, $line, [], new ValaIrStaticCallExpression($line, $line, [], 'Zval.from_null', [])),
        ]);
    }

    private function maybeOptimizeLocalIntZvals(ValaIrFunctionDeclaration $fn): ValaIrFunctionDeclaration
    {
        $array = $fn->toArray();
        $returnType = (string) ($array['returnType'] ?? '');
        if (!in_array($returnType, ['Zval', 'void'], true)) {
            return $fn;
        }
        if (($array['tryCatch'] ?? null) !== null) {
            return $fn;
        }

        $env = [];
        $params = $this->declaredScalarParamsForFunction($fn);

        $stmts = $this->prop($fn, 'stmts');
        if (!$this->stmtListHasLocalIntZvalShape($stmts)) {
            return $fn;
        }

        $typedStmts = $returnType === 'void'
            ? $this->translateLocalIntStmtListPartial($stmts, $env, $params)
            : $this->translateLocalIntStmtList($stmts, $env, $params);
        if ($typedStmts === null || $typedStmts === $stmts) {
            return $fn;
        }
        return $fn->withStmts($typedStmts);
    }

    private function stmtListHasLocalIntZvalShape(array $stmts): bool
    {
        foreach ($stmts as $stmt) {
            if ($stmt instanceof ValaIrVariableDeclaration && (string) $this->prop($stmt, 'typeName') === 'Zval') {
                return true;
            }
            if ($stmt instanceof ValaIrIfStatement) {
                if ($this->stmtListHasLocalIntZvalShape($this->prop($stmt, 'stmts')) || $this->stmtListHasLocalIntZvalShape($this->prop($stmt, 'elseStmts'))) {
                    return true;
                }
                foreach ($this->prop($stmt, 'elseifs') as $elseif) {
                    if (is_array($elseif) && $this->stmtListHasLocalIntZvalShape(is_array($elseif['stmts'] ?? null) ? $elseif['stmts'] : [])) {
                        return true;
                    }
                }
            }
            if ($stmt instanceof ValaIrWhileStatement && $this->stmtListHasLocalIntZvalShape($this->prop($stmt, 'stmts'))) {
                return true;
            }
        }
        return false;
    }

    private function translateLocalIntStmtList(array $stmts, array &$env, array $params): ?array
    {
        $out = [];
        foreach ($stmts as $stmt) {
            $translated = $this->translateLocalIntStmt($stmt, $env, $params);
            if ($translated === null) {
                return null;
            }
            foreach ($translated as $item) {
                $out[] = $item;
            }
        }
        return $out;
    }

    private function translateLocalIntStmtListPartial(array $stmts, array &$env, array $params): ?array
    {
        $out = [];
        foreach ($stmts as $stmt) {
            $translated = $this->translateLocalIntStmt($stmt, $env, $params);
            if ($translated === null) {
                if ($this->stmtReferencesLocalInt($stmt, $env) || $this->stmtWritesLocalInt($stmt, $env)) {
                    return null;
                }
                $out[] = $stmt;
                continue;
            }
            foreach ($translated as $item) {
                $out[] = $item;
            }
        }
        return $out;
    }

    private function translateLocalIntStmt(object $stmt, array &$env, array $params): ?array
    {
        $array = $stmt->toArray();
        $line = (int) ($array['startLine'] ?? 0);
        $endLine = (int) ($array['endLine'] ?? $line);
        $comments = $this->prop($stmt, 'comments');

        if ($stmt instanceof ValaIrVariableDeclaration) {
            $name = (string) $this->prop($stmt, 'name');
            $typeName = (string) $this->prop($stmt, 'typeName');
            if ($typeName !== 'Zval') {
                return $this->exprReferencesLocalInt($this->prop($stmt, 'expr'), $env) ? null : [$stmt];
            }
            $expr = $this->prop($stmt, 'expr');
            if (($params[$name] ?? null) === 'int' && $this->isMatchingRuntimeArgument($expr, $name, $params)) {
                $env[$name] = 'int64';
                return [new ValaIrVariableDeclaration($line, $endLine, $comments, 'int64', $name, new ValaIrMethodCallExpression($line, $endLine, [], $expr, 'as_int', []))];
            }
            if ($this->isZvalUndefined($expr)) {
                $env[$name] = 'int64';
                return [new ValaIrVariableDeclaration($line, $endLine, $comments, 'int64', $name, new ValaIrIntLiteral($line, $endLine, [], 0))];
            }
            $typedExpr = $this->translateLocalIntExpr($expr, $env);
            if ($typedExpr === null) {
                return null;
            }
            $env[$name] = 'int64';
            return [new ValaIrVariableDeclaration($line, $endLine, $comments, 'int64', $name, $typedExpr)];
        }

        if ($stmt instanceof ValaIrAssignmentStatement) {
            $name = (string) $this->prop($stmt, 'name');
            if (!isset($env[$name])) {
                return null;
            }
            $expr = $this->translateLocalIntExpr($this->prop($stmt, 'expr'), $env);
            return $expr !== null ? [new ValaIrAssignmentStatement($line, $endLine, $comments, $name, $expr)] : null;
        }

        if ($stmt instanceof ValaIrIfStatement) {
            $condition = $this->translateLocalBoolExpr($this->prop($stmt, 'condition'), $env);
            if ($condition === null) {
                return null;
            }
            $branchEnv = $env;
            $branch = $this->translateLocalIntStmtList($this->prop($stmt, 'stmts'), $branchEnv, $params);
            $elseEnv = $env;
            $elseBranch = $this->translateLocalIntStmtList($this->prop($stmt, 'elseStmts'), $elseEnv, $params);
            if ($branch === null || $elseBranch === null) {
                return null;
            }
            $elseifs = [];
            foreach ($this->prop($stmt, 'elseifs') as $elseif) {
                if (!is_array($elseif) || !is_object($elseif['condition'] ?? null) || !is_array($elseif['stmts'] ?? null)) {
                    return null;
                }
                $elseifCondition = $this->translateLocalBoolExpr($elseif['condition'], $env);
                $elseifEnv = $env;
                $elseifStmts = $this->translateLocalIntStmtList($elseif['stmts'], $elseifEnv, $params);
                if ($elseifCondition === null || $elseifStmts === null) {
                    return null;
                }
                $elseifs[] = ['condition' => $elseifCondition, 'stmts' => $elseifStmts];
            }
            return [new ValaIrIfStatement($line, $endLine, $comments, $condition, $branch, $elseifs, $elseBranch)];
        }

        if ($stmt instanceof ValaIrWhileStatement) {
            $condition = $this->translateLocalBoolExpr($this->prop($stmt, 'condition'), $env);
            $bodyEnv = $env;
            $body = $this->translateLocalIntStmtList($this->prop($stmt, 'stmts'), $bodyEnv, $params);
            return $condition !== null && $body !== null ? [new ValaIrWhileStatement($line, $endLine, $comments, $condition, $body)] : null;
        }

        if ($stmt instanceof ValaIrCallStatement) {
            $originalArgs = $this->prop($stmt, 'args');
            $args = $this->translateLocalIntBoundaryArgs($originalArgs, $env, $line, $endLine);
            if ($args === null) {
                return null;
            }
            if ($args === $originalArgs) {
                return [$stmt];
            }
            return [$stmt->withCalleeAndArgs((string) ($array['callee'] ?? ''), $args)];
        }

        if ($stmt instanceof ValaIrReturnStatement) {
            $coerceReturnType = strtolower((string) ($array['coerceReturnType'] ?? ''));
            $expr = $this->prop($stmt, 'expr');
            if (!is_object($expr)) {
                return null;
            }
            if ($coerceReturnType === '' || $coerceReturnType === 'int' || $coerceReturnType === 'integer') {
                $typedExpr = $this->translateLocalIntExpr($expr, $env);
                if ($typedExpr === null) {
                    return null;
                }
                return [new ValaIrReturnStatement($line, $endLine, $comments, new ValaIrStaticCallExpression($line, $endLine, [], 'Zval.from_int', [$typedExpr]))];
            }
            if ($coerceReturnType === 'float' || $coerceReturnType === 'double') {
                $typedExpr = $this->translateLocalFloatReturnExpr($expr, $env);
                if ($typedExpr === null) {
                    return null;
                }
                return [new ValaIrReturnStatement($line, $endLine, $comments, new ValaIrStaticCallExpression($line, $endLine, [], 'Zval.from_float', [$typedExpr]))];
            }
            return null;
        }

        return null;
    }

    private function translateLocalIntBoundaryArgs(array $args, array $env, int $line, int $endLine): ?array
    {
        $out = [];
        foreach ($args as $arg) {
            if (!is_object($arg)) {
                $out[] = $arg;
                continue;
            }
            if (!$this->exprReferencesLocalInt($arg, $env)) {
                $out[] = $arg;
                continue;
            }
            $typed = $this->translateLocalIntExpr($arg, $env);
            if ($typed === null) {
                return null;
            }
            $out[] = new ValaIrStaticCallExpression($line, $endLine, [], 'Zval.from_int', [$typed]);
        }
        return $out;
    }

    private function exprReferencesLocalInt(mixed $expr, array $env): bool
    {
        return $this->valueReferencesLocalInt($expr, $env, false);
    }

    private function stmtReferencesLocalInt(object $stmt, array $env): bool
    {
        return $this->valueReferencesLocalInt($stmt, $env, true);
    }

    private function valueReferencesLocalInt(mixed $value, array $env, bool $skipStatementWriteName): bool
    {
        if (is_array($value)) {
            foreach ($value as $item) {
                if ($this->valueReferencesLocalInt($item, $env, false)) {
                    return true;
                }
            }
            return false;
        }
        if (!is_object($value)) {
            return false;
        }
        if ($value instanceof ValaIrVariableReference) {
            return isset($env[(string) $this->prop($value, 'name')]);
        }

        $ref = new ReflectionObject($value);
        foreach ($ref->getProperties() as $prop) {
            if ($skipStatementWriteName && $prop->getName() === 'name' && ($value instanceof ValaIrAssignmentStatement || $value instanceof ValaIrVariableDeclaration)) {
                continue;
            }
            if ($this->valueReferencesLocalInt($prop->getValue($value), $env, false)) {
                return true;
            }
        }
        return false;
    }

    private function stmtWritesLocalInt(object $stmt, array $env): bool
    {
        if ($stmt instanceof ValaIrAssignmentStatement) {
            return isset($env[(string) $this->prop($stmt, 'name')]);
        }
        if ($stmt instanceof ValaIrVariableDeclaration) {
            return isset($env[(string) $this->prop($stmt, 'name')]);
        }
        return false;
    }

    private function translateLocalBoolExpr(object $expr, array $env): ?object
    {
        if ($expr instanceof ValaIrBoolLiteral) {
            return $expr;
        }
        if ($expr instanceof ValaIrStaticCallExpression && ($expr->toArray()['callee'] ?? null) === 'Zval.from_bool') {
            $args = $this->prop($expr, 'args');
            return count($args) === 1 && $args[0] instanceof ValaIrBoolLiteral ? $args[0] : null;
        }
        if (!$expr instanceof ValaIrMethodCallExpression) {
            return null;
        }
        $methodName = (string) ($expr->toArray()['methodName'] ?? '');
        if ($methodName === 'to_bool') {
            $args = $this->prop($expr, 'args');
            return $args === [] ? $this->translateLocalBoolExpr($this->prop($expr, 'receiver'), $env) : null;
        }
        $op = match ($methodName) {
            'smaller_or_equal' => '<=',
            'smaller' => '<',
            'greater_or_equal' => '>=',
            'greater' => '>',
            'identical', 'equal' => '==',
            'not_identical', 'not_equal' => '!=',
            default => null,
        };
        if ($op === null) {
            return null;
        }
        $args = $this->prop($expr, 'args');
        if (count($args) !== 1) {
            return null;
        }
        $left = $this->translateLocalIntExpr($this->prop($expr, 'receiver'), $env);
        $right = $this->translateLocalIntExpr($args[0], $env);
        if ($left === null || $right === null) {
            return null;
        }
        $array = $expr->toArray();
        return new ValaIrBinaryExpression((int) ($array['startLine'] ?? 0), (int) ($array['endLine'] ?? 0), [], $op, $left, $right);
    }

    private function translateLocalIntExpr(object $expr, array $env): ?object
    {
        if ($expr instanceof ValaIrIntLiteral) {
            return $expr;
        }
        if ($expr instanceof ValaIrVariableReference) {
            $name = (string) $this->prop($expr, 'name');
            return isset($env[$name]) ? $expr : null;
        }

        $array = $expr->toArray();
        $line = (int) ($array['startLine'] ?? 0);
        $endLine = (int) ($array['endLine'] ?? $line);

        if ($expr instanceof ValaIrStaticCallExpression) {
            $callee = (string) ($array['callee'] ?? '');
            $args = $this->prop($expr, 'args');
            if ($callee === 'Zval.from_int' && count($args) === 1) {
                return $this->translateLocalIntExpr($args[0], $env);
            }
            return null;
        }

        if ($expr instanceof ValaIrMethodCallExpression) {
            $methodName = (string) ($array['methodName'] ?? '');
            if ($methodName === 'as_int') {
                $args = $this->prop($expr, 'args');
                return $args === [] ? $this->translateLocalIntExpr($this->prop($expr, 'receiver'), $env) : null;
            }
            $op = match ($methodName) {
                'add' => '+',
                'subtract' => '-',
                'multiply' => '*',
                default => null,
            };
            if ($op === null) {
                return null;
            }
            $args = $this->prop($expr, 'args');
            if (count($args) !== 1) {
                return null;
            }
            $left = $this->translateLocalIntExpr($this->prop($expr, 'receiver'), $env);
            $right = $this->translateLocalIntExpr($args[0], $env);
            return $left !== null && $right !== null ? new ValaIrBinaryExpression($line, $endLine, [], $op, $left, $right) : null;
        }

        return null;
    }

    private function declaredScalarParamsForFunction(ValaIrFunctionDeclaration $fn): array
    {
        $paramTypes = $fn->toArray()['paramTypes'] ?? [];
        return is_array($paramTypes) ? $paramTypes : [];
    }

    private function isMatchingRuntimeArgument(object $expr, string $name, array $params): bool
    {
        if (!$expr instanceof ValaIrStaticCallExpression || ($expr->toArray()['callee'] ?? null) !== 'PhpefRuntimeCore.argument') {
            return false;
        }
        $paramNames = array_keys($params);
        $index = array_search($name, $paramNames, true);
        if ($index === false) {
            return false;
        }
        $args = $this->prop($expr, 'args');
        return count($args) === 2 && $this->isVariableReference($args[0], 'args') && $this->intLiteralValue($args[1]) === $index;
    }

    private function translateLocalFloatReturnExpr(object $expr, array $env): ?object
    {
        if ($expr instanceof ValaIrMethodCallExpression && ($expr->toArray()['methodName'] ?? null) === 'to_float_zval') {
            $args = $this->prop($expr, 'args');
            return $args === [] ? $this->translateLocalIntExpr($this->prop($expr, 'receiver'), $env) : null;
        }
        return $this->translateLocalIntExpr($expr, $env);
    }

    private function isZvalUndefined(object $expr): bool
    {
        return $expr instanceof ValaIrStaticCallExpression && ($expr->toArray()['callee'] ?? null) === 'Zval.undefined';
    }

    private function isVariableReference(object $expr, string $name): bool
    {
        return $expr instanceof ValaIrVariableReference && (string) $this->prop($expr, 'name') === $name;
    }

    private function intLiteralValue(object $expr): ?int
    {
        return $expr instanceof ValaIrIntLiteral ? (int) $this->prop($expr, 'value') : null;
    }

    private function singleStaticIntCallInHotLoop(array $stmts): ?array
    {
        $whileStmts = null;
        foreach ($stmts as $stmt) {
            if ($stmt instanceof ValaIrWhileStatement) {
                if ($whileStmts !== null) {
                    return null;
                }
                $whileStmts = $this->prop($stmt, 'stmts');
            }
        }
        if ($whileStmts === null) {
            return null;
        }

        $target = null;
        foreach ($whileStmts as $stmt) {
            if (!$stmt instanceof ValaIrCallStatement) {
                continue;
            }
            $array = $stmt->toArray();
            $callee = (string) ($array['callee'] ?? '');
            if (!str_ends_with($callee, '__phpefc_int')) {
                continue;
            }
            $args = $this->prop($stmt, 'args');
            if ($args === [] || !$this->allIntLiterals($args)) {
                continue;
            }
            if ($target !== null) {
                return null;
            }
            $target = ['callee' => $callee, 'args' => $args];
        }
        return $target;
    }

    private function allIntLiterals(array $args): bool
    {
        foreach ($args as $arg) {
            if (!$arg instanceof ValaIrIntLiteral) {
                return false;
            }
        }
        return true;
    }


    private function maybeOptimizeDynamicSelfTimingLoop(ValaIrFunctionDeclaration $fn): ValaIrFunctionDeclaration
    {
        $array = $fn->toArray();
        if (($array['returnType'] ?? '') !== 'Zval' || ($array['tryCatch'] ?? null) !== null) {
            return $fn;
        }

        $shape = $this->dynamicSelfTimingLoopShape($this->prop($fn, 'stmts'));
        if ($shape === null) {
            return $fn;
        }

        $timeFastPath = $this->timeFastPathFromCallExpression($shape['timeExpression']);
        if ($timeFastPath === null) {
            return $fn;
        }

        $line = (int) ($array['startLine'] ?? 0);
        $endLine = (int) ($array['endLine'] ?? $line);
        $names = $shape['names'];
        $methodSelectorName = (string) $names['methodSelector'];
        $windowStartName = (string) $names['windowStart'];
        $overallStartName = (string) $names['overallStart'];
        $sampleCountName = (string) $names['sampleCount'];
        $iterationCountName = (string) $names['iterationCount'];
        $iterationSumName = (string) $names['iterationSum'];
        $currentTimeName = (string) $names['currentTime'];
        $v = fn (string $var): ValaIrVariableReference => new ValaIrVariableReference($line, $endLine, [], $var);
        $f = fn (float $value): ValaIrFloatLiteral => new ValaIrFloatLiteral($line, $endLine, [], $value);
        $time = fn (): ValaIrStaticCallExpression => new ValaIrStaticCallExpression($line, $endLine, [], (string) $timeFastPath['callee'], []);

        $optimized = $fn->withStmts([
            new ValaIrVariableDeclaration($line, $endLine, [], 'string', $methodSelectorName, new ValaIrMethodCallExpression($line, $endLine, [], $this->prop($shape['methodDeclaration'], 'expr'), 'to_php_string', [])),
            new ValaIrVariableDeclaration($line, $endLine, [], 'double', $windowStartName, $time()),
            new ValaIrVariableDeclaration($line, $endLine, [], 'double', $overallStartName, $v($windowStartName)),
            new ValaIrVariableDeclaration($line, $endLine, [], 'double', $sampleCountName, $f(0.0)),
            new ValaIrVariableDeclaration($line, $endLine, [], 'double', $iterationCountName, $f(0.0)),
            new ValaIrVariableDeclaration($line, $endLine, [], 'double', $iterationSumName, $f(0.0)),
            new ValaIrVariableDeclaration($line, $endLine, [], 'double', $currentTimeName, $f(0.0)),
            new ValaIrWhileStatement($line, $endLine, [], new ValaIrBoolLiteral($line, $endLine, [], true), [
                new ValaIrCallStatement($line, $endLine, [], 'PhpefRuntimeCore.call_instance_method', [new ValaIrVariableReference($line, $endLine, [], '__phpefc_this'), $v($methodSelectorName), new ValaIrArrayCreationExpression($line, $endLine, [], 'Zval', []), $shape['dispatchScope']]),
                new ValaIrAssignmentStatement($line, $endLine, [], $iterationCountName, new ValaIrBinaryExpression($line, $endLine, [], '+', $v($iterationCountName), $f(1.0))),
                new ValaIrAssignmentStatement($line, $endLine, [], $currentTimeName, $time()),
                new ValaIrIfStatement($line, $endLine, [], new ValaIrBinaryExpression($line, $endLine, [], '>', new ValaIrBinaryExpression($line, $endLine, [], '-', $v($currentTimeName), $v($windowStartName)), $f(1.0)), [
                    new ValaIrAssignmentStatement($line, $endLine, [], $sampleCountName, new ValaIrBinaryExpression($line, $endLine, [], '+', $v($sampleCountName), $f(1.0))),
                    new ValaIrAssignmentStatement($line, $endLine, [], $iterationSumName, new ValaIrBinaryExpression($line, $endLine, [], '+', $v($iterationSumName), $v($iterationCountName))),
                    new ValaIrAssignmentStatement($line, $endLine, [], $windowStartName, $time()),
                    new ValaIrAssignmentStatement($line, $endLine, [], $iterationCountName, $f(0.0)),
                ]),
                new ValaIrIfStatement($line, $endLine, [], new ValaIrBinaryExpression($line, $endLine, [], '>', new ValaIrBinaryExpression($line, $endLine, [], '-', $v($currentTimeName), $v($overallStartName)), $f(2.0)), [
                    new ValaIrReturnStatement($line, $endLine, [], new ValaIrStaticCallExpression($line, $endLine, [], $shape['returnCallee'], [new ValaIrStaticCallExpression($line, $endLine, [], 'Zval.from_float', [new ValaIrBinaryExpression($line, $endLine, [], '/', $v($iterationSumName), $v($sampleCountName))])])),
                ]),
            ]),
        ]);

        return $this->withProp($optimized, 'defaultReturnExpr', null);
    }

    /** @return array{methodDeclaration: ValaIrVariableDeclaration, names: array<string,string>, dispatchScope: ValaIrStringLiteral, returnCallee: string, timeExpression: object}|null */
    private function dynamicSelfTimingLoopShape(array $stmts): ?array
    {
        if (count($stmts) < 13) {
            return null;
        }
        $methodSelectorName = $this->zvalDeclarationName($stmts[0] ?? null);
        $windowStartName = $this->zvalDeclarationName($stmts[1] ?? null);
        $overallStartName = $this->zvalDeclarationName($stmts[2] ?? null);
        $sampleCountName = $this->zvalDeclarationName($stmts[3] ?? null);
        $iterationCountName = $this->zvalDeclarationName($stmts[4] ?? null);
        $iterationSumName = $this->zvalDeclarationName($stmts[5] ?? null);
        $currentTimeName = $this->zvalDeclarationName($stmts[6] ?? null);
        foreach ([$methodSelectorName, $windowStartName, $overallStartName, $sampleCountName, $iterationCountName, $iterationSumName, $currentTimeName] as $name) {
            if ($name === null) {
                return null;
            }
        }
        $timeExpression = $this->prop($stmts[7], 'expr');
        if (!$timeExpression instanceof ValaIrStaticCallExpression) {
            return null;
        }
        if (!$stmts[12] instanceof ValaIrWhileStatement) {
            return null;
        }
        $whileStmts = $this->prop($stmts[12], 'stmts');
        if (count($whileStmts) < 5 || !$whileStmts[0] instanceof ValaIrCallStatement) {
            return null;
        }
        $dispatch = $whileStmts[0];
        if (($dispatch->toArray()['callee'] ?? null) !== 'PhpefRuntimeCore.call_instance_method') {
            return null;
        }
        $dispatchArgs = $dispatch->args();
        if (count($dispatchArgs) !== 4 || !$this->isVariableReference($dispatchArgs[0], '__phpefc_this') || !$this->isVariableOrToPhpStringOf($dispatchArgs[1], $methodSelectorName) || !$dispatchArgs[2] instanceof ValaIrArrayCreationExpression || !$dispatchArgs[3] instanceof ValaIrStringLiteral) {
            return null;
        }
        if ($this->arrayCreationItems($dispatchArgs[2]) !== []) {
            return null;
        }
        if (!$whileStmts[4] instanceof ValaIrIfStatement) {
            return null;
        }
        $returnStmts = $this->prop($whileStmts[4], 'stmts');
        if (count($returnStmts) !== 1 || !$returnStmts[0] instanceof ValaIrReturnStatement) {
            return null;
        }
        $returnExpr = $this->prop($returnStmts[0], 'expr');
        if (!$returnExpr instanceof ValaIrStaticCallExpression) {
            return null;
        }
        $returnArgs = $returnExpr->args();
        if (count($returnArgs) !== 1 || !$this->isDivisionCallBetweenVariables($returnArgs[0], $iterationSumName, $sampleCountName)) {
            return null;
        }
        $returnCallee = (string) ($returnExpr->toArray()['callee'] ?? '');
        return $returnCallee !== '' ? [
            'methodDeclaration' => $stmts[0],
            'names' => [
                'methodSelector' => $methodSelectorName,
                'windowStart' => $windowStartName,
                'overallStart' => $overallStartName,
                'sampleCount' => $sampleCountName,
                'iterationCount' => $iterationCountName,
                'iterationSum' => $iterationSumName,
                'currentTime' => $currentTimeName,
            ],
            'dispatchScope' => $dispatchArgs[3],
            'returnCallee' => $returnCallee,
            'timeExpression' => $timeExpression,
        ] : null;
    }

    private function timeFastPathFromFunctionStatements(array $stmts): ?array
    {
        foreach ($stmts as $stmt) {
            if ($stmt instanceof ValaIrVariableDeclaration || $stmt instanceof ValaIrAssignmentStatement) {
                $expr = $this->prop($stmt, 'expr');
                $fastPath = is_object($expr) ? $this->timeFastPathFromCallExpression($expr) : null;
                if ($fastPath !== null) {
                    return $fastPath;
                }
            }
        }
        return null;
    }

    private function timeFastPathFromCallExpression(object $expr): ?array
    {
        if (!$expr instanceof ValaIrStaticCallExpression || $this->funcResolver === null) {
            return null;
        }
        $phpName = $this->phpRuntimeFunctionNameFromCallee((string) ($expr->toArray()['callee'] ?? ''));
        if ($phpName === null) {
            return null;
        }
        $argumentTypes = $this->typedArgumentTypesFromRuntimeArgs($expr->args());
        if ($argumentTypes === null) {
            return null;
        }
        $fastPath = $this->funcResolver->resolveTypedFastPath($phpName, $argumentTypes, ['native'], 'double');
        if ($fastPath === null || ($fastPath['passArguments'] ?? false)) {
            return null;
        }
        return $fastPath;
    }

    private function phpRuntimeFunctionNameFromCallee(string $callee): ?string
    {
        $lastDot = strrpos($callee, '.');
        $runtimeName = $lastDot === false ? $callee : substr($callee, $lastDot + 1);
        if (!str_starts_with($runtimeName, 'phpfunc_') || str_contains($runtimeName, '_typed_')) {
            return null;
        }
        return substr($runtimeName, strlen('phpfunc_'));
    }

    /** @return string[]|null */
    private function typedArgumentTypesFromRuntimeArgs(array $args): ?array
    {
        $types = [];
        foreach ($args as $arg) {
            $type = $this->typedArgumentTypeFromRuntimeArg($arg);
            if ($type === null) {
                return null;
            }
            $types[] = $type;
        }
        return $types;
    }

    private function typedArgumentTypeFromRuntimeArg(object $arg): ?string
    {
        if (!$arg instanceof ValaIrStaticCallExpression) {
            return null;
        }
        $callee = (string) ($arg->toArray()['callee'] ?? '');
        $args = $arg->args();
        if ($callee === 'Zval.from_bool' && count($args) === 1 && $args[0] instanceof ValaIrBoolLiteral) {
            return 'bool:' . (((bool) ($args[0]->toArray()['value'] ?? false)) ? 'true' : 'false');
        }
        if ($callee === 'Zval.from_int' && count($args) === 1 && $args[0] instanceof ValaIrIntLiteral) {
            return 'int';
        }
        if ($callee === 'Zval.from_float' && count($args) === 1 && $args[0] instanceof ValaIrFloatLiteral) {
            return 'double';
        }
        if ($callee === 'Zval.from_string' && count($args) === 1 && $args[0] instanceof ValaIrStringLiteral) {
            return 'string';
        }
        return null;
    }


    private function isNamedDeclaration(ValaIrVariableDeclaration $decl, string $name, string $typeName): bool
    {
        return (string) $this->prop($decl, 'name') === $name && (string) $this->prop($decl, 'typeName') === $typeName;
    }


    private function zvalDeclarationName(mixed $stmt): ?string
    {
        if (!$stmt instanceof ValaIrVariableDeclaration || (string) $this->prop($stmt, 'typeName') !== 'Zval') {
            return null;
        }
        $name = (string) $this->prop($stmt, 'name');
        return $name !== '' ? $name : null;
    }

    private function isMethodCallOnVariable(object $expr, string $receiverName, string $methodName, string $argName): bool
    {
        if (!$expr instanceof ValaIrMethodCallExpression || ($expr->toArray()['methodName'] ?? null) !== $methodName) {
            return false;
        }
        if (!$this->isVariableReference($this->prop($expr, 'receiver'), $receiverName)) {
            return false;
        }
        $args = $this->prop($expr, 'args');
        return count($args) === 1 && $this->isVariableReference($args[0], $argName);
    }


    private function isDivisionCallBetweenVariables(object $expr, string $leftName, string $rightName): bool
    {
        return $this->isMethodCallOnVariable($expr, $leftName, 'divide', $rightName);
    }

    private function isVariableOrToPhpStringOf(object $expr, string $name): bool
    {
        if ($this->isVariableReference($expr, $name)) {
            return true;
        }
        if (!$expr instanceof ValaIrMethodCallExpression || ($expr->toArray()['methodName'] ?? null) !== 'to_php_string') {
            return false;
        }
        $args = $this->prop($expr, 'args');
        return $args === [] && $this->isVariableReference($this->prop($expr, 'receiver'), $name);
    }
    private function optimizeObjectChildren(object $node, array $classMap, ?string $currentClass, ?string $currentReturnType): object
    {
        if ($node instanceof ValaIrFunctionDeclaration) {
            $optimizedFunction = $this->maybeOptimizeStaticHotLoopHelper($node);
            if ($optimizedFunction !== $node) {
                return $this->optimizeObjectChildren($optimizedFunction, $classMap, $currentClass, $currentReturnType);
            }
            $optimizedFunction = $this->maybeOptimizeDynamicSelfTimingLoop($node);
            if ($optimizedFunction !== $node) {
                return $this->optimizeObjectChildren($optimizedFunction, $classMap, $currentClass, $currentReturnType);
            }
            $optimizedFunction = $this->maybeOptimizeLocalIntZvals($node);
            if ($optimizedFunction !== $node) {
                return $this->optimizeObjectChildren($optimizedFunction, $classMap, $currentClass, $currentReturnType);
            }
        }

        $changed = false;
        $clone = null;
        $ref = new ReflectionObject($node);
        foreach ($ref->getProperties() as $prop) {
            $value = $prop->getValue($node);
            $optimized = $this->optimizeValue($value, $classMap, $currentClass, $currentReturnType);
            if ($optimized !== $value) {
                if ($clone === null) {
                    $clone = clone $node;
                }
                $prop->setValue($clone, $optimized);
                $changed = true;
            }
        }
        return $changed && $clone !== null ? $clone : $node;
    }

    private function optimizeValue(mixed $value, array $classMap, ?string $currentClass, ?string $currentReturnType): mixed
    {
        if (is_array($value)) {
            return $this->optimizeArray($value, $classMap, $currentClass, $currentReturnType);
        }
        if (is_object($value)) {
            return $this->optimizeNode($value, $classMap, $currentClass, $currentReturnType);
        }
        return $value;
    }

    private function optimizeArray(array $items, array $classMap, ?string $currentClass, ?string $currentReturnType): array
    {
        $changed = false;
        $optimized = [];
        foreach ($items as $key => $item) {
            $newItem = $this->optimizeValue($item, $classMap, $currentClass, $currentReturnType);
            $changed = $changed || $newItem !== $item;
            $optimized[$key] = $newItem;
        }
        return $changed ? $optimized : $items;
    }

    private function staticMethodFastPath(object $call, array $classMap): ?object
    {
        if (!$call instanceof ValaIrCallStatement && !$call instanceof ValaIrStaticCallExpression) {
            return null;
        }

        $array = $call->toArray();
        if (($array['callee'] ?? null) !== 'PhpefRuntimeCore.call_static_method') {
            return null;
        }

        $args = $call->args();
        if (count($args) !== 4) {
            return null;
        }

        $className = $this->stringLiteralValue($args[0]);
        $methodName = $this->stringLiteralValue($args[1]);
        $currentClassScope = $this->stringLiteralValue($args[3]);
        if ($className === null || $methodName === null || $currentClassScope === null) {
            return null;
        }
        if (!$args[2] instanceof ValaIrArrayCreationExpression) {
            return null;
        }

        $method = $this->knownSameFileStaticMethod($classMap, $className, $methodName);
        if ($method === null) {
            return null;
        }
        if (!$this->staticMethodAccessIsProven($method, $className, $currentClassScope)) {
            return null;
        }

        $callArgs = $this->arrayCreationItems($args[2]);
        if (count($callArgs) < (int) ($method['minArgs'] ?? -1) || count($callArgs) > (int) ($method['maxArgs'] ?? -1)) {
            return null;
        }
        // PHP-near generated static methods currently expose their PHP parameters
        // directly for zero-argument methods. Keep non-zero argument calls on the
        // RuntimeCore ABI until argument mapping/coercion is proven here.
        if (count($callArgs) !== 0) {
            return null;
        }

        $callee = (string) ($method['handlerSymbol'] ?? $method['valaName'] ?? '');
        if ($callee === '') {
            return null;
        }

        return $call->withCalleeAndArgs($callee, []);
    }

    private function staticMethodAccessIsProven(array $method, string $className, string $currentClassScope): bool
    {
        // A global Class::method() call must keep going through RuntimeCore so
        // class lookup/visibility errors remain centralized. Direct lowering is
        // only proven while compiling inside a PHP class scope.
        if ($currentClassScope === '') {
            return false;
        }

        $visibility = strtolower((string) ($method['visibility'] ?? 'public'));
        if ($visibility === 'public') {
            return true;
        }
        // Conservative for private/protected: only same-class scope is proven.
        return strtolower($currentClassScope) === strtolower($className);
    }

    private function instanceMethodFastPath(object $call, array $classMap): ?object
    {
        if (!$call instanceof ValaIrCallStatement && !$call instanceof ValaIrStaticCallExpression) {
            return null;
        }

        $array = $call->toArray();
        if (($array['callee'] ?? null) !== 'PhpefRuntimeCore.call_instance_method') {
            return null;
        }

        $args = $call->args();
        if (count($args) !== 4) {
            return null;
        }
        if (!$this->isVariableReference($args[0], '__phpefc_this')) {
            return null;
        }

        $methodName = $this->stringLiteralValue($args[1]);
        $currentClassScope = $this->stringLiteralValue($args[3]);
        if ($methodName === null || $currentClassScope === null || $currentClassScope === '') {
            return null;
        }
        if (!$args[2] instanceof ValaIrArrayCreationExpression) {
            return null;
        }

        $method = $this->knownSameFileInstanceMethod($classMap, $currentClassScope, $methodName);
        if ($method === null) {
            return null;
        }
        if (!$this->instanceMethodAccessIsProven($method, $currentClassScope)) {
            return null;
        }

        $callArgs = $this->arrayCreationItems($args[2]);
        if (count($callArgs) < (int) ($method['minArgs'] ?? -1) || count($callArgs) > (int) ($method['maxArgs'] ?? -1)) {
            return null;
        }

        $callee = (string) ($method['handlerSymbol'] ?? $method['valaName'] ?? '');
        if ($callee === '') {
            return null;
        }

        return $call->withCalleeAndArgs($callee, [$args[0], $args[2]]);
    }

    private function instanceMethodAccessIsProven(array $method, string $currentClassScope): bool
    {
        // Direct self-call lowering is only semantics-preserving for private
        // methods of the current class. Public/protected calls must keep PHP's
        // dynamic dispatch/override behaviour in RuntimeCore.
        if (strtolower((string) ($method['visibility'] ?? 'public')) !== 'private') {
            return false;
        }
        $phpName = (string) ($method['phpName'] ?? '');
        if (!str_contains($phpName, '::')) {
            return false;
        }
        [$declaringClass] = explode('::', $phpName, 2);
        return strtolower($declaringClass) === strtolower($currentClassScope);
    }

    private function knownSameFileStaticMethod(array $classMap, string $className, string $methodSelectorName): ?array
    {
        $class = $classMap[strtolower($className)] ?? null;
        if (!is_array($class)) {
            return null;
        }
        foreach ($class as $method) {
            if (!is_array($method)) {
                continue;
            }
            if (strtolower((string) ($method['phpMethodName'] ?? '')) !== strtolower($methodSelectorName)) {
                continue;
            }
            if (($method['static'] ?? false) !== true) {
                continue;
            }
            return $method;
        }
        return null;
    }

    private function knownSameFileInstanceMethod(array $classMap, string $className, string $methodSelectorName): ?array
    {
        $class = $classMap[strtolower($className)] ?? null;
        if (!is_array($class)) {
            return null;
        }
        foreach ($class as $method) {
            if (!is_array($method)) {
                continue;
            }
            if (strtolower((string) ($method['phpMethodName'] ?? '')) !== strtolower($methodSelectorName)) {
                continue;
            }
            if (($method['instance'] ?? false) !== true) {
                continue;
            }
            return $method;
        }
        return null;
    }

    private function classMap(array $stmts): array
    {
        $map = [];
        foreach ($stmts as $stmt) {
            if (!$stmt instanceof ValaIrClassDeclaration) {
                continue;
            }
            $class = $stmt->toArray();
            $phpClassName = (string) ($class['phpClassName'] ?? $class['phpName'] ?? '');
            if ($phpClassName === '') {
                continue;
            }
            $map[strtolower($phpClassName)] = is_array($class['methodSummaries'] ?? null) ? $class['methodSummaries'] : [];
        }
        return $map;
    }

    private function stringLiteralValue(object $expr): ?string
    {
        if (!$expr instanceof ValaIrStringLiteral) {
            return null;
        }
        return (string) ($expr->toArray()['value'] ?? '');
    }

    private function arrayCreationItems(ValaIrArrayCreationExpression $expr): array
    {
        return $this->prop($expr, 'items');
    }

    private function prop(object $object, string $name): mixed
    {
        $ref = new ReflectionProperty($object, $name);
        return $ref->getValue($object);
    }

    private function withProp(object $object, string $name, mixed $value): object
    {
        $clone = clone $object;
        $ref = new ReflectionProperty($clone, $name);
        $ref->setValue($clone, $value);
        return $clone;
    }
}
