<?php

use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;

/**
 * @class PhpAstProcessor
 * Thin nikic/php-parser adapter plus line-aware AST-to-PHP printing.
 */
final class PhpAstProcessor
{
    /**
     * Parse PHP source text into a nikic/php-parser AST.
     */
    public static function phpSource2ast(string $code): array
    {
        $parser = (new ParserFactory())->createForNewestSupportedVersion();
        try {
            return $parser->parse($code);
        } catch (Error $error) {
            throw new RuntimeException("Parse error: {$error->getMessage()}", 0, $error);
        }
    }

    /**
     * Serialize one AST. This is deliberately only the raw AST payload.
     */
    public static function ast2string(mixed $ast): string
    {
        return serialize($ast);
    }

    /**
     * Unserialize one raw AST payload.
     */
    public static function string2ast(string $string): mixed
    {
        return unserialize($string);
    }

    /**
     * Convert one AST back to PHP source with line-aware comment spacing.
     */
    public static function ast2php(mixed $ast): string
    {
        return self::ast2phpLineAware($ast);
    }

    /**
     * Convert one raw serialized AST payload back to PHP source.
     */
    public static function string2php(string $string): string
    {
        return self::ast2php(self::string2ast($string));
    }

    private static function ast2phpLineAware(mixed $ast): string
    {
        if (!is_array($ast)) {
            $prettyPrinter = new PrettyPrinter\Standard();
            return $prettyPrinter->prettyPrintFile($ast);
        }

        $prettyPrinter = new PrettyPrinter\Standard();
        $lines = ['<?php'];
        $currentLine = 2;

        foreach ($ast as $stmt) {
            $targetLine = self::nodeStartLineIncludingComments($stmt);
            while ($currentLine < $targetLine) {
                $lines[] = '';
                $currentLine++;
            }

            $code = $prettyPrinter->prettyPrint([$stmt]);
            $code = self::expandLeadingCommentGaps($code, $stmt);
            $stmtLines = $code === '' ? [] : explode("\n", $code);
            foreach ($stmtLines as $line) {
                $lines[] = $line;
                $currentLine++;
            }
        }

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

    private static function nodeStartLineIncludingComments(mixed $node): int
    {
        $line = self::nodeStartLine($node);
        foreach (self::nodeComments($node) as $comment) {
            $line = min($line, self::commentStartLine($comment));
        }
        return $line;
    }

    private static function expandLeadingCommentGaps(string $code, mixed $node): string
    {
        $comments = self::nodeComments($node);
        if ($comments === []) {
            return $code;
        }

        $lines = explode("\n", $code);
        $result = [];
        $cursor = 0;
        $count = count($comments);

        foreach ($comments as $idx => $comment) {
            $commentLineCount = self::commentLineCount($comment);
            for ($i = 0; $i < $commentLineCount && $cursor < count($lines); $i++) {
                $result[] = $lines[$cursor++];
            }

            $nextStartLine = $idx + 1 < $count
                ? self::commentStartLine($comments[$idx + 1])
                : self::nodeStartLine($node);
            $gap = $nextStartLine - self::commentEndLine($comment) - 1;
            for ($i = 0; $i < $gap; $i++) {
                $result[] = '';
            }
        }

        while ($cursor < count($lines)) {
            $result[] = $lines[$cursor++];
        }

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

    private static function nodeStartLine(mixed $node): int
    {
        return is_object($node) && method_exists($node, 'getStartLine') ? $node->getStartLine() : 1;
    }

    private static function nodeComments(mixed $node): array
    {
        return is_object($node) && method_exists($node, 'getComments') ? $node->getComments() : [];
    }

    private static function commentStartLine(mixed $comment): int
    {
        return is_object($comment) && method_exists($comment, 'getStartLine') ? $comment->getStartLine() : 1;
    }

    private static function commentEndLine(mixed $comment): int
    {
        return is_object($comment) && method_exists($comment, 'getEndLine') ? $comment->getEndLine() : self::commentStartLine($comment);
    }

    private static function commentLineCount(mixed $comment): int
    {
        $fromLines = self::commentEndLine($comment) - self::commentStartLine($comment) + 1;
        if (is_object($comment) && method_exists($comment, 'getText')) {
            $fromText = substr_count($comment->getText(), "\n") + 1;
            return max(1, $fromLines, $fromText);
        }
        return max(1, $fromLines);
    }
}
