<?php

/** Coordinates typing-zval2static sub-passes and owns typing-specific marker/counting. */
final class ValaIrTypingZval2StaticOptimizer
{
    // List of pass objects exposing optimize(object): object.
    private array $passes;
    private int $lastBeforeCount = 0;
    private int $lastAfterCount = 0;
    private array $lastAfterBreakdown = [];

    /** Create the runner with default typing sub-passes or an injected pass list. */
    public function __construct(?array $passes = null)
    {
        $this->passes = $passes ?? [
            new ValaIrTypingFunctionLocalOptimizer(),
            new ValaIrTypingPhpfuncCallOptimizer(),
        ];
    }

    /** Run all typing sub-passes and append the final zval counter marker. */
    public function optimize(object $valaIr): object
    {
        $this->lastBeforeCount = $this->countCandidates($valaIr);
        $optimized = $valaIr;

        foreach ($this->passes as $pass) {
            if (!method_exists($pass, "optimize")) {
                throw new RuntimeException("Typing optimizer pass without optimize(): " . get_debug_type($pass));
            }
            $optimized = $pass->optimize($optimized);
        }

        $this->lastAfterCount = $this->countCandidates($optimized);
        $this->lastAfterBreakdown = $this->countCandidateBreakdown($optimized);
        $marker = "// phpefc optimizer typing-zval2static result: " . $this->getLastResultText();
        $optimized = $this->appendMarker($optimized, $marker);
        $breakdown = $this->lastBreakdownText();
        if ($breakdown !== "") {
            $optimized = $this->appendMarker($optimized, "// phpefc optimizer typing-zval2static breakdown: " . $breakdown);
        }
        return $optimized;
    }

    /** Return the candidate count before the last optimize() run. */
    public function getLastBeforeCount(): int
    {
        return $this->lastBeforeCount;
    }

    /** Return the candidate count after the last optimize() run. */
    public function getLastAfterCount(): int
    {
        return $this->lastAfterCount;
    }

    /** Return the human-readable counter text from the last optimize() run. */
    public function getLastResultText(): string
    {
        return "{$this->lastAfterCount}/{$this->lastBeforeCount} zvals left";
    }

    /** Return a compact categorized view of remaining dynamic Zval sites. */
    public function getLastBreakdownText(): string
    {
        return $this->lastBreakdownText();
    }

    /** Return whether the last optimize() run left dynamic zval candidates. */
    public function hasRemainingDynamicZvals(): bool
    {
        return $this->lastAfterCount > 0;
    }

    private function countToken(string $value): int
    {
        $matches = [];
        return preg_match_all("/\\bZval\\b/", $value, $matches);
    }

    private function countCandidates(mixed $node): int
    {
        if (is_object($node)) {
            return $this->countCandidates($this->toDebugArray($node));
        }
        if (!is_array($node) || ($node["kind"] ?? null) === "Comment") {
            return 0;
        }

        $count = 0;
        $kind = (string) ($node["kind"] ?? "");
        if ($kind === "ArrayCreationExpression" && (string) ($node["elementTypeName"] ?? $node["elementType"] ?? "") === "Zval") {
            $count++;
        }
        $skipVariableDeclarationType = $this->isArgumentFallbackDeclaration($node);
        if ($kind === "StaticCallExpression" && $this->isRuntimeZvalReturningCall((string) ($node["callee"] ?? ""))) {
            $count++;
        }

        foreach ($node as $key => $value) {
            if ($this->isSignatureOnlyField($kind, (string) $key)) {
                continue;
            }
            if (is_string($value)) {
                if ((($key === "typeName" && !$skipVariableDeclarationType) || $key === "returnType") && str_contains($value, "Zval")) {
                    $count += $this->countToken($value);
                }
                if ($key === "callee" && str_starts_with($value, "Zval.")) {
                    $count++;
                }
                continue;
            }
            $count += $this->countCandidates($value);
        }
        return $count;
    }



    private function countCandidateBreakdown(mixed $node): array
    {
        $breakdown = [
            "decl" => 0,
            "return" => 0,
            "zval.array" => 0,
            "zval.from" => 0,
            "zval.method" => 0,
            "zval.runtime-return" => 0,
            "runtime" => 0,
            "type" => 0,
        ];
        $this->collectCandidateBreakdown($node, $breakdown);
        return array_filter($breakdown, static fn (int $count): bool => $count > 0);
    }


    private function collectCandidateBreakdown(mixed $node, array &$breakdown): void
    {
        if (is_object($node)) {
            $this->collectCandidateBreakdown($this->toDebugArray($node), $breakdown);
            return;
        }
        if (!is_array($node)) {
            return;
        }
        $kind = (string) ($node["kind"] ?? "");
        if ($kind === "Comment") {
            return;
        }
        $skipVariableDeclarationType = $this->isArgumentFallbackDeclaration($node);
        if ($kind === "VariableDeclaration" && !$skipVariableDeclarationType && (string) ($node["typeName"] ?? "") === "Zval") {
            $breakdown["decl"]++;
        }
        if ($kind === "ArrayCreationExpression" && (string) ($node["elementTypeName"] ?? $node["elementType"] ?? "") === "Zval") {
            $breakdown["zval.array"]++;
        }
        if ($kind === "StaticCallExpression" && $this->isRuntimeZvalReturningCall((string) ($node["callee"] ?? ""))) {
            $breakdown["zval.runtime-return"]++;
        }
        foreach ($node as $key => $value) {
            if ($this->isSignatureOnlyField($kind, (string) $key)) {
                continue;
            }
            if (is_string($value)) {
                if ((($key === "typeName" && !$skipVariableDeclarationType) || $key === "returnType") && str_contains($value, "Zval")) {
                    $breakdown["type"] += $this->countToken($value);
                }
                if ($key === "callee" && str_starts_with($value, "Zval.from_")) {
                    $breakdown["zval.from"]++;
                } elseif ($key === "callee" && str_starts_with($value, "Zval.")) {
                    $breakdown["runtime"]++;
                }
                if ($key === "methodName" && in_array($value, ["add", "subtract", "multiply", "divide", "concat", "greater", "greater_or_equal", "smaller", "smaller_or_equal", "equal", "not_equal", "identical", "not_identical", "as_int", "as_float", "as_string", "to_bool"], true)) {
                    $breakdown["zval.method"]++;
                }
                continue;
            }
            $this->collectCandidateBreakdown($value, $breakdown);
        }
    }



    /**
     * Zval function/method signature slots are ABI fallback surface, not dynamic
     * optimization debt. Count Zval values at call sites and in function bodies,
     * but do not count declaration params/returns that must remain available for
     * other callers.
     */
    private function isSignatureOnlyField(string $kind, string $key): bool
    {
        if ($kind !== "FunctionDeclaration") {
            return false;
        }
        return in_array($key, ["params", "parameters", "valaParams", "returnType", "paramTypes"], true);
    }

    private function isArgumentFallbackDeclaration(array $node): bool
    {
        if ((string) ($node["kind"] ?? "") !== "VariableDeclaration" || (string) ($node["typeName"] ?? "") !== "Zval") {
            return false;
        }
        $expr = $node["expr"] ?? null;
        if (is_object($expr)) {
            $expr = $this->toDebugArray($expr);
        }
        return is_array($expr)
            && (string) ($expr["kind"] ?? "") === "StaticCallExpression"
            && (string) ($expr["callee"] ?? "") === "PhpefRuntimeCore.argument";
    }

    private function isRuntimeZvalReturningCall(string $callee): bool
    {
        return in_array($callee, [
            "PhpefRuntimeCore.call_function",
            "PhpefRuntimeCore.call_instance_method",
            "PhpefRuntimeCore.call_static_method",
            "PhpefRuntimeCore.new_object",
        ], true);
    }

    private function lastBreakdownText(): string
    {
        if ($this->lastAfterBreakdown === []) {
            return '';
        }
        $parts = [];
        foreach ($this->lastAfterBreakdown as $name => $count) {
            $parts[] = $name . '=' . $count;
        }
        return 'remaining ' . implode(', ', $parts);
    }

    /** Convert an IR object to its debug array representation. */
    private function toDebugArray(object $ir): array
    {
        if (!method_exists($ir, "toArray")) {
            throw new RuntimeException("Expected IR object with toArray(), got " . get_debug_type($ir));
        }
        return $ir->toArray();
    }

    /** Append a comment marker to a program object. */
    private function appendMarker(object $ir, string $marker): object
    {
        if (!method_exists($ir, "withAppendedComment")) {
            throw new RuntimeException("Expected IR program with withAppendedComment(), got " . get_debug_type($ir));
        }

        return $ir->withAppendedComment(new ValaIrComment(
            "OptimizerMarker",
            $marker,
            0,
            0,
        ));
    }
}
