<?php

/**
 * Precompute explicitly allowlisted runtime calls with fully static arguments.
 *
 * The pass evaluates only hard-coded safe RuntimeEmbed/RuntimeNative calls by
 * compiling and running a tiny Vala probe through valac. Runtime results are
 * read from stdout and converted back into equivalent ValaIR Zval factories.
 */
final class ValaIrPrecomputeOptimizer
{
    private int $lastPrecomputedCount = 0;
    private int $lastSkippedCount = 0;
    private array $cache = [];

    /** Apply precompute rewrites and append a result marker when possible. */
    public function optimize(object $valaIr): object
    {
        $this->lastPrecomputedCount = 0;
        $this->lastSkippedCount = 0;
        $optimized = $this->optimizeNode($valaIr);

        if (!method_exists($optimized, 'withAppendedComment')) {
            return $optimized;
        }

        return $optimized->withAppendedComment(new ValaIrComment(
            'OptimizerMarker',
            '// phpefc optimizer precompute result: ' . $this->getLastResultText(),
            0,
            0,
        ));
    }

    /** Return how many calls were precomputed in the last run. */
    public function getLastPrecomputedCount(): int
    {
        return $this->lastPrecomputedCount;
    }

    /** Return how many allowlisted-looking calls could not be precomputed. */
    public function getLastSkippedCount(): int
    {
        return $this->lastSkippedCount;
    }

    /** Return the human-readable result text for markers/tests. */
    public function getLastResultText(): string
    {
        return $this->lastPrecomputedCount . ' calls precomputed, ' . $this->lastSkippedCount . ' skipped';
    }

    private function optimizeNode(object $node): object
    {
        if ($node instanceof ValaIrProgram) {
            return $this->withProp($node, 'stmts', $this->optimizeStmtList($this->prop($node, 'stmts')));
        }
        if ($node instanceof ValaIrClassDeclaration) {
            return $this->withProp($node, 'methods', $this->optimizeStmtList($this->prop($node, 'methods')));
        }
        if ($node instanceof ValaIrFunctionDeclaration) {
            return $this->withProp($node, 'stmts', $this->optimizeStmtList($this->prop($node, 'stmts')));
        }
        if ($node instanceof ValaIrVariableDeclaration || $node instanceof ValaIrAssignmentStatement) {
            return $this->withProp($node, 'expr', $this->optimizeExpr($this->prop($node, 'expr')));
        }
        if ($node instanceof ValaIrReturnStatement) {
            $expr = $this->prop($node, 'expr');
            return is_object($expr) ? $this->withProp($node, 'expr', $this->optimizeExpr($expr)) : $node;
        }
        if ($node instanceof ValaIrCallStatement) {
            return $this->withProp($node, 'args', $this->optimizeExprList($this->prop($node, 'args')));
        }
        if ($node instanceof ValaIrIfStatement) {
            $optimized = $this->withProp($node, 'condition', $this->optimizeExpr($this->prop($node, 'condition')));
            $optimized = $this->withProp($optimized, 'stmts', $this->optimizeStmtList($this->prop($optimized, 'stmts')));
            $optimized = $this->withProp($optimized, 'elseifs', $this->optimizeElseifs($this->prop($optimized, 'elseifs')));
            return $this->withProp($optimized, 'elseStmts', $this->optimizeStmtList($this->prop($optimized, 'elseStmts')));
        }
        if ($node instanceof ValaIrWhileStatement) {
            $optimized = $this->withProp($node, 'condition', $this->optimizeExpr($this->prop($node, 'condition')));
            return $this->withProp($optimized, 'stmts', $this->optimizeStmtList($this->prop($optimized, 'stmts')));
        }
        if ($node instanceof ValaIrForeachValueStatement) {
            $optimized = $this->withProp($node, 'expr', $this->optimizeExpr($this->prop($node, 'expr')));
            return $this->withProp($optimized, 'stmts', $this->optimizeStmtList($this->prop($optimized, 'stmts')));
        }
        if ($node instanceof ValaIrTryCatchStatement) {
            $optimized = $this->withProp($node, 'tryStmts', $this->optimizeStmtList($this->prop($node, 'tryStmts')));
            return $this->withProp($optimized, 'catchStmts', $this->optimizeStmtList($this->prop($optimized, 'catchStmts')));
        }
        return $node;
    }

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

    private function optimizeElseifs(array $elseifs): array
    {
        $changed = false;
        $optimized = [];
        foreach ($elseifs as $elseif) {
            if (!is_array($elseif)) {
                $optimized[] = $elseif;
                continue;
            }
            $newElseif = $elseif;
            if (is_object($newElseif['condition'] ?? null)) {
                $newElseif['condition'] = $this->optimizeExpr($newElseif['condition']);
            }
            $newElseif['stmts'] = $this->optimizeStmtList(is_array($newElseif['stmts'] ?? null) ? $newElseif['stmts'] : []);
            $changed = $changed || $newElseif !== $elseif;
            $optimized[] = $newElseif;
        }
        return $changed ? $optimized : $elseifs;
    }

    private function optimizeExpr(object $expr): object
    {
        if ($expr instanceof ValaIrStaticCallExpression) {
            $optimized = $this->withProp($expr, 'args', $this->optimizeExprList($this->prop($expr, 'args')));
            $precomputed = $this->precomputeStaticCall($optimized);
            return $precomputed ?? $optimized;
        }
        if ($expr instanceof ValaIrMethodCallExpression) {
            $optimized = $this->withProp($expr, 'receiver', $this->optimizeExpr($this->prop($expr, 'receiver')));
            return $this->withProp($optimized, 'args', $this->optimizeExprList($this->prop($optimized, 'args')));
        }
        if ($expr instanceof ValaIrConstructExpression) {
            return $this->withProp($expr, 'args', $this->optimizeExprList($this->prop($expr, 'args')));
        }
        if ($expr instanceof ValaIrArrayCreationExpression || $expr instanceof ValaIrArrayLiteral) {
            return $this->withProp($expr, 'items', $this->optimizeExprList($this->prop($expr, 'items')));
        }
        if ($expr instanceof ValaIrArrayItem) {
            $optimized = $this->withProp($expr, 'key', $this->maybeOptimizeNullableExpr($this->prop($expr, 'key')));
            return $this->withProp($optimized, 'value', $this->optimizeExpr($this->prop($optimized, 'value')));
        }
        if ($expr instanceof ValaIrBinaryExpression) {
            $optimized = $this->withProp($expr, 'left', $this->optimizeExpr($this->prop($expr, 'left')));
            return $this->withProp($optimized, 'right', $this->optimizeExpr($this->prop($optimized, 'right')));
        }
        if ($expr instanceof ValaIrTernaryExpression) {
            $optimized = $this->withProp($expr, 'condition', $this->optimizeExpr($this->prop($expr, 'condition')));
            $optimized = $this->withProp($optimized, 'ifExpr', $this->optimizeExpr($this->prop($optimized, 'ifExpr')));
            return $this->withProp($optimized, 'elseExpr', $this->optimizeExpr($this->prop($optimized, 'elseExpr')));
        }
        return $expr;
    }

    private function maybeOptimizeNullableExpr(mixed $expr): mixed
    {
        return is_object($expr) ? $this->optimizeExpr($expr) : $expr;
    }

    private function optimizeExprList(array $exprs): array
    {
        $changed = false;
        $optimized = [];
        foreach ($exprs as $expr) {
            if (!is_object($expr)) {
                $optimized[] = $expr;
                continue;
            }
            $newExpr = $this->optimizeExpr($expr);
            $changed = $changed || $newExpr !== $expr;
            $optimized[] = $newExpr;
        }
        return $changed ? $optimized : $exprs;
    }

    private function precomputeStaticCall(ValaIrStaticCallExpression $call): ?object
    {
        $array = $call->toArray();
        $callee = (string) ($array['callee'] ?? '');
        $args = $this->prop($call, 'args');
        $callSpec = $this->precomputableCallSpec($callee);
        if ($callSpec === null) {
            return null;
        }

        $staticArgs = $this->staticArgsForSpec($callSpec, $args);
        if ($staticArgs === null) {
            $this->lastSkippedCount++;
            return null;
        }

        $result = $this->runPrecomputeProbe($callee, $callSpec, $staticArgs);
        if ($result === null) {
            $this->lastSkippedCount++;
            return null;
        }

        $replacement = $this->zvalExpressionFromResult($result, (int) ($array['startLine'] ?? 0), (int) ($array['endLine'] ?? 0));
        if ($replacement === null) {
            $this->lastSkippedCount++;
            return null;
        }
        $this->lastPrecomputedCount++;
        return $replacement;
    }

    private function precomputableCallSpec(string $callee): ?array
    {
        return match ($callee) {
            'PhpefRuntimeNativeStandard.phpfunc_constant_typed_zval_arg1_string' => [
                'args' => ['string'],
                'return' => 'zval',
                'packages' => ['phpef-runtime-core', 'phpef-runtime-embed', 'phpef-runtime-native-standard'],
            ],
            'PhpefRuntimeNativeStandard.phpfunc_constant' => [
                'args' => ['zval:string'],
                'return' => 'zval',
                'packages' => ['phpef-runtime-core', 'phpef-runtime-embed', 'phpef-runtime-native-standard'],
            ],
            'PhpefRuntimeEmbed.phpfunc_constant' => [
                'args' => ['zval:string'],
                'return' => 'zval',
                'packages' => ['phpef-runtime-core', 'phpef-runtime-embed'],
            ],
            default => null,
        };
    }

    private function staticArgsForSpec(array $spec, array $args): ?array
    {
        $argTypes = is_array($spec['args'] ?? null) ? $spec['args'] : [];
        if (count($argTypes) !== count($args)) {
            return null;
        }
        $staticArgs = [];
        foreach ($argTypes as $index => $type) {
            $arg = $args[$index] ?? null;
            if ($type === 'string') {
                $value = $this->stringLiteralValue($arg);
                if ($value === null) {
                    return null;
                }
                $staticArgs[] = ['type' => 'string', 'value' => $value];
                continue;
            }
            if ($type === 'zval:string') {
                $value = $this->zvalStringLiteralValue($arg);
                if ($value === null) {
                    return null;
                }
                $staticArgs[] = ['type' => 'zval:string', 'value' => $value];
                continue;
            }
            return null;
        }
        return $staticArgs;
    }

    private function stringLiteralValue(mixed $arg): ?string
    {
        if (!$arg instanceof ValaIrStringLiteral) {
            return null;
        }
        $array = $arg->toArray();
        return is_string($array['value'] ?? null) ? $array['value'] : null;
    }

    private function zvalStringLiteralValue(mixed $arg): ?string
    {
        if (!$arg instanceof ValaIrStaticCallExpression) {
            return null;
        }
        $array = $arg->toArray();
        if (($array['callee'] ?? null) !== 'Zval.from_string') {
            return null;
        }
        $args = $this->prop($arg, 'args');
        if (count($args) !== 1) {
            return null;
        }
        return $this->stringLiteralValue($args[0]);
    }

    private function runPrecomputeProbe(string $callee, array $spec, array $staticArgs): ?array
    {
        $cacheKey = sha1($callee . "\0" . serialize($staticArgs));
        if (array_key_exists($cacheKey, $this->cache)) {
            return $this->cache[$cacheKey];
        }

        $tmpDir = sys_get_temp_dir() . '/phpefc-precompute-' . getmypid() . '-' . bin2hex(random_bytes(4));
        if (!mkdir($tmpDir, 0700, true)) {
            return $this->cache[$cacheKey] = null;
        }
        $src = $tmpDir . '/probe.vala';
        $bin = $tmpDir . '/probe';
        try {
            if (file_put_contents($src, $this->probeSource($callee, $staticArgs)) === false) {
                return $this->cache[$cacheKey] = null;
            }
            $packages = is_array($spec['packages'] ?? null) ? $spec['packages'] : [];
            $packageArgs = '';
            foreach ($packages as $package) {
                $packageArgs .= ' --pkg ' . escapeshellarg((string) $package);
            }
            $compileCmd = 'valac' . $packageArgs . ' ' . escapeshellarg($src) . ' -o ' . escapeshellarg($bin) . ' 2>&1';
            $compileOutput = [];
            $compileStatus = 0;
            exec($compileCmd, $compileOutput, $compileStatus);
            if ($compileStatus !== 0) {
                return $this->cache[$cacheKey] = null;
            }
            $runOutput = [];
            $runStatus = 0;
            exec(escapeshellarg($bin) . ' 2>&1', $runOutput, $runStatus);
            if ($runStatus !== 0) {
                return $this->cache[$cacheKey] = null;
            }
            $result = $this->parseProbeOutput($runOutput);
            return $this->cache[$cacheKey] = $result;
        } finally {
            $this->removeTree($tmpDir);
        }
    }

    private function probeSource(string $callee, array $staticArgs): string
    {
        $argExprs = [];
        foreach ($staticArgs as $arg) {
            $value = (string) ($arg['value'] ?? '');
            $argExprs[] = match ($arg['type'] ?? '') {
                'string' => $this->valaStringLiteral($value),
                'zval:string' => 'PhpefRuntimeCore.Zval.from_string(' . $this->valaStringLiteral($value) . ')',
                default => 'null',
            };
        }
        $call = $callee . '(' . implode(', ', $argExprs) . ')';
        return "int main(string[] args) {\n"
            . "    PhpefRuntimeEmbed.engine_start(args);\n"
            . "    var result = " . $call . ";\n"
            . "    switch (result.kind) {\n"
            . "        case PhpefRuntimeCore.ValueKind.STRING:\n"
            . "            string s = result.as_string() ?? \"\";\n"
            . "            stdout.printf(\"STRING\\t%s\\n\", Base64.encode(s.data));\n"
            . "            break;\n"
            . "        case PhpefRuntimeCore.ValueKind.INT:\n"
            . "            stdout.printf(\"INT\\t%lld\\n\", result.as_int());\n"
            . "            break;\n"
            . "        case PhpefRuntimeCore.ValueKind.BOOL:\n"
            . "            stdout.printf(\"BOOL\\t%s\\n\", result.as_bool() ? \"true\" : \"false\");\n"
            . "            break;\n"
            . "        case PhpefRuntimeCore.ValueKind.NULL:\n"
            . "            stdout.printf(\"NULL\\n\");\n"
            . "            break;\n"
            . "        default:\n"
            . "            PhpefRuntimeEmbed.engine_stop();\n"
            . "            return 3;\n"
            . "    }\n"
            . "    PhpefRuntimeEmbed.engine_stop();\n"
            . "    return 0;\n"
            . "}\n";
    }

    private function parseProbeOutput(array $lines): ?array
    {
        foreach ($lines as $line) {
            $line = rtrim((string) $line, "\r\n");
            if ($line === '') {
                continue;
            }
            $parts = explode("\t", $line, 2);
            $kind = $parts[0];
            $value = $parts[1] ?? '';
            if ($kind === 'STRING') {
                $decoded = base64_decode($value, true);
                return is_string($decoded) ? ['kind' => 'string', 'value' => $decoded] : null;
            }
            if ($kind === 'INT' && preg_match('/^-?[0-9]+$/', $value) === 1) {
                return ['kind' => 'int', 'value' => (int) $value];
            }
            if ($kind === 'BOOL') {
                return ['kind' => 'bool', 'value' => $value === 'true'];
            }
            if ($kind === 'NULL') {
                return ['kind' => 'null'];
            }
            return null;
        }
        return null;
    }

    private function zvalExpressionFromResult(array $result, int $startLine, int $endLine): ?object
    {
        return match ($result['kind'] ?? null) {
            'string' => new ValaIrStaticCallExpression($startLine, $endLine, [], 'Zval.from_string', [
                new ValaIrStringLiteral($startLine, $endLine, [], (string) ($result['value'] ?? '')),
            ]),
            'int' => new ValaIrStaticCallExpression($startLine, $endLine, [], 'Zval.from_int', [
                new ValaIrIntLiteral($startLine, $endLine, [], (int) ($result['value'] ?? 0)),
            ]),
            'bool' => new ValaIrStaticCallExpression($startLine, $endLine, [], 'Zval.from_bool', [
                new ValaIrBoolLiteral($startLine, $endLine, [], (bool) ($result['value'] ?? false)),
            ]),
            'null' => new ValaIrStaticCallExpression($startLine, $endLine, [], 'Zval.from_null', []),
            default => null,
        };
    }

    private function valaStringLiteral(string $value): string
    {
        $escaped = '';
        $length = strlen($value);
        for ($i = 0; $i < $length; $i++) {
            $ord = ord($value[$i]);
            $escaped .= match ($value[$i]) {
                "\\" => "\\\\",
                '"' => "\\\"",
                "\n" => "\\n",
                "\r" => "\\r",
                "\t" => "\\t",
                default => $ord < 0x20 ? sprintf('\\x%02x', $ord) : $value[$i],
            };
        }
        return '"' . $escaped . '"';
    }

    private function removeTree(string $path): void
    {
        if (!is_dir($path)) {
            return;
        }
        $entries = scandir($path);
        if (!is_array($entries)) {
            return;
        }
        foreach ($entries as $entry) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }
            $full = $path . DIRECTORY_SEPARATOR . $entry;
            if (is_dir($full) && !is_link($full)) {
                $this->removeTree($full);
            } else {
                @unlink($full);
            }
        }
        @rmdir($path);
    }

    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
    {
        $current = $this->prop($object, $name);
        if ($current === $value) {
            return $object;
        }
        $clone = clone $object;
        $ref = new ReflectionProperty($clone, $name);
        $ref->setValue($clone, $value);
        return $clone;
    }
}
