<?php

declare(strict_types=1);

/*
 * This file is part of the box project.
 *
 * (c) Kevin Herrera <kevin@herrera.io>
 *     Théo Fidry <theo.fidry@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace KevinGH\Box\Composer;

use Humbug\PhpScoper\Autoload\ComposerFileHasher;
use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator;
use Humbug\PhpScoper\Symbol\SymbolsRegistry;
use KevinGH\Box\NotInstantiable;
use UnexpectedValueException;
use function array_map;
use function explode;
use function implode;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_replace;
use const PHP_EOL;

final class AutoloadDumper
{
    use NotInstantiable;

    private const PACKAGE_PATH_REGEX = '~^%s/(?<vendor>[^/]+?/[^/]+?)/(?<path>.+?)$~';

    /**
     * @param string[] $excludedComposerAutoloadFiles
     */
    public static function generateAutoloadStatements(
        SymbolsRegistry $symbolsRegistry,
        string $vendorDir,
        array $excludedComposerAutoloadFiles,
        string $autoloadContents,
    ): string {
        if (0 === $symbolsRegistry->count()) {
            return $autoloadContents;
        }

        $excludedComposerAutoloadFileHashes = self::getExcludedComposerAutoloadFileHashes(
            $vendorDir,
            $excludedComposerAutoloadFiles,
        );

        $autoloadContents = self::extractInlinedAutoloadContents($autoloadContents);
        $scoperStatements = self::getOriginalScoperAutoloaderContents(
            $symbolsRegistry,
            $excludedComposerAutoloadFileHashes,
        );

        $indentedAutoloadContents = self::fixInlinedAutoloadIndent(
            $autoloadContents,
            self::getLoaderStatementIndent($scoperStatements),
        );

        $mergedAutoloadContents = preg_replace(
            '/(\s*\\$loader \= .*autoload\.php.*)/',
            $indentedAutoloadContents,
            $scoperStatements,
        );

        return self::cleanupDuplicateLineReturns($mergedAutoloadContents);
    }

    /**
     * @param string[] $excludedComposerAutoloadFiles
     */
    private static function getExcludedComposerAutoloadFileHashes(
        string $vendorDir,
        array $excludedComposerAutoloadFiles,
    ): array {
        $fileHashGenerator = new ComposerFileHasher(
            '',
            $excludedComposerAutoloadFiles,
            sprintf(
                self::PACKAGE_PATH_REGEX,
                $vendorDir,
            ),
        );

        return $fileHashGenerator->generateHashes();
    }

    private static function extractInlinedAutoloadContents(string $autoloadContents): string
    {
        $autoloadContents = str_replace('<?php', '', $autoloadContents);

        return preg_replace(
            '/return (ComposerAutoloaderInit.+::getLoader\(\));/',
            '\$loader = $1;',
            $autoloadContents,
        );
    }

    private static function getOriginalScoperAutoloaderContents(
        SymbolsRegistry $symbolsRegistry,
        array $excludedComposerAutoloadFileHashes,
    ): string {
        $generator = new ScoperAutoloadGenerator($symbolsRegistry, $excludedComposerAutoloadFileHashes);
        $scoperStatements = $generator->dump();

        return preg_replace(
            '/scoper\-autoload\.php \@generated by PhpScoper/',
            '@generated by Humbug Box',
            $scoperStatements,
        );
    }

    private static function getLoaderStatementIndent(string $scoperStatements): string
    {
        if (1 !== preg_match('/(?<indent> *)\\$loader \= .*autoload\.php.*/', $scoperStatements, $matches)) {
            throw new UnexpectedValueException('Could not process the scoper autoloader statements');
        }

        return $matches['indent'];
    }

    private static function fixInlinedAutoloadIndent(string $autoloadContents, string $indent): string
    {
        $lines = explode(PHP_EOL, $autoloadContents);

        $indentedLines = array_map(
            static fn (string $line) => '' === $line ? $line : $indent.$line,
            $lines,
        );

        return implode(PHP_EOL, $indentedLines);
    }

    private static function cleanupDuplicateLineReturns(string $value): string
    {
        return preg_replace(
            '/\n{2,}/m',
            PHP_EOL.PHP_EOL,
            $value,
        );
    }
}
