#!/usr/bin/env php
<?php declare(strict_types=1);

use Phan\AST\TolerantASTConverter\Shim;
use Phan\CodeBase;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Func;
use Phan\Language\Element\GlobalConstant;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\FQSEN\FullyQualifiedGlobalConstantName;

require_once dirname(__DIR__) . '/src/requirements.php';
require_once dirname(__DIR__) . '/src/Phan/Bootstrap.php';

// If php-ast isn't loaded already, then load this file to generate equivalent
// class, constant, and function definitions.
Shim::load();

/**
 * Generates PHP stubs for extensions that can be used in the autoload_internal_extension_signatures `.phan/config.php` setting.
 * These are regular PHP files stubbing PHP modules, containing empty method implementations, etc.
 *
 * Configured stubs can be used in IDEs, analysis, etc. where extensions aren't installed or enabled (e.g. Xdebug)
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod, PhanPluginNoCommentOnPublicMethod
 */
class StubsGenerator
{

    /**
     * @return void (does not return)
     */
    public static function printHelpAndExit(string $message = '', int $exit_code = 1) : void
    {
        if ($message !== '') {
            echo "$message\n";
        }
        global $argv;
        $prog_name = $argv[0];
        echo <<<EOT
Usage: $prog_name [--opts]
 -h, --help
  Print this help message

 -e, --extension
  Print stubs for only the given PECL or built in extension (e.g. 'ast', 'pcntl')
EOT;
        exit($exit_code);
    }

    /**
     * The main function of the `make_stubs` script.
     * See `make_stubs --help` for usage.
     */
    public static function main() : void
    {
        $options = getopt('he:', ['help', 'extension:']);
        if (isset($options['h']) || isset($options['help'])) {
            self::printHelpAndExit('', 0);
        }

        $code_base = require_once(dirname(__DIR__) . '/src/codebase.php');

        $extension_name = $options['e'] ?? $options['extension'] ?? null;
        if (is_string($extension_name)) {
            self::printStubsForExtension($extension_name, $code_base);
            return;
        }
        self::printAllStubs($code_base);
    }

    private static function printStubsForExtension(string $extension_name, CodeBase $code_base) : void
    {
        $stub_collection = new StubCollection($code_base);
        $reflection_extension = new ReflectionExtension($extension_name);
        $extension_version = $reflection_extension->getVersion();

        self::recordClassStubsForExtension($stub_collection, $reflection_extension, $code_base);
        self::recordGlobalFunctionStubsForExtension($stub_collection, $reflection_extension, $code_base);
        self::recordGlobalConstantStubsForExtension($stub_collection, $reflection_extension, $code_base);

        echo "<" . "?php\n";
        echo "// These stubs were generated by the phan stub generator.\n";
        echo "// @phan-stub-for-extension $extension_name@$extension_version\n";
        echo "\n";

        echo $stub_collection->toString();
    }

    private static function recordClassStubsForExtension(
        StubCollection $stub_collection,
        ReflectionExtension $reflection_extension,
        CodeBase $code_base
    ) : void {
        foreach ($reflection_extension->getClassNames() as $class_name) {
            try {
                $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class_name);
            } catch (Exception $e) {
                // only possible if module info is wrong
                fwrite(STDERR, "Failed to parse fqsen of class $class_name : {$e->getMessage()}\n");
                continue;
            }
            if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
                fwrite(STDERR, "Failed to find class $class_fqsen\n");
                continue;
            }
            $stub_collection->addClazz($code_base->getClassByFQSEN($class_fqsen));
        }
    }

    private static function recordGlobalFunctionStubsForExtension(
        StubCollection $stub_collection,
        ReflectionExtension $reflection_extension,
        CodeBase $code_base
    ) : void {
        foreach ($reflection_extension->getFunctions() as $function_name => $unused_reflection_function) {
            try {
                $function_fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($function_name);
            } catch (Exception $e) {
                // only possible if module info is wrong
                fwrite(STDERR, "Failed to parse fqsen of function $function_name : {$e->getMessage()}\n");
                continue;
            }
            if (!$code_base->hasFunctionWithFQSEN($function_fqsen)) {
                fwrite(STDERR, "Failed to find function $function_fqsen\n");
                continue;
            }
            $stub_collection->addFunc($code_base->getFunctionByFQSEN($function_fqsen));
        }
    }

    private static function recordGlobalConstantStubsForExtension(
        StubCollection $stub_collection,
        ReflectionExtension $reflection_extension,
        CodeBase $code_base
    ) : void {
        foreach ($reflection_extension->getConstants() as $constant_name => $_) {
            try {
                $const_fqsen = FullyQualifiedGlobalConstantName::fromFullyQualifiedString($constant_name);
            } catch (Exception $e) {
                // only possible if module info is invalid
                fwrite(STDERR, "Failed to parse fqsen of global constant $constant_name : {$e->getMessage()}\n");
                continue;
            }
            if (!$code_base->hasGlobalConstantWithFQSEN($const_fqsen)) {
                fwrite(STDERR, "Failed to find global constant $const_fqsen\n");
                continue;
            }
            $stub_collection->addGlobalConstant($code_base->getGlobalConstantByFQSEN($const_fqsen));
        }
    }

    public static function printAllStubs(CodeBase $code_base) : void
    {
        $code_base->eagerlyLoadAllSignatures();
        $stub_collection = new StubCollection($code_base);
        $class_map = $code_base->getInternalClassMap();

        echo "<" . "?php\n";
        echo "// These stubs were generated by the phan stub generator.\n";

        foreach ($class_map as $class) {
            $stub_collection->addClazz($class);
        }

        $function_map = $code_base->getFunctionMap();
        foreach ($function_map as $function) {
            if ($function->getFQSEN()->isAlternate()) {
                continue;
            }
            $stub_collection->addFunc($function);
        }

        $const_map = $code_base->getGlobalConstantMap();

        foreach ($const_map as $const) {
            $stub_collection->addGlobalConstant($const);
        }
        echo $stub_collection->toString();
    }
}

StubsGenerator::main();

/**
 * A representation of the collection of stubs for elements of a PHP module(a.k.a. extension).
 */
class StubCollection
{
    /** @var CodeBase represents the known state of the code base we're extracting the stubs from. */
    private $code_base;

    public function __construct(CodeBase $code_base)
    {
        $this->code_base = $code_base;
    }

    /** @var string[][] a list of class stubs for a PHP module */
    public $class_stubs = [];
    /** @var string[][] a list of function stubs for a PHP module */
    public $function_stubs = [];
    /** @var string[][] a list of global constant stubs for a PHP module */
    public $global_constant_stubs = [];

    public function addClazz(Clazz $class) : void
    {
        list($namespace, $name) = $class->toStubInfo($this->code_base);
        $this->class_stubs[$namespace][(string)$class->getFQSEN()] = $name;
    }

    public function addGlobalConstant(GlobalConstant $global_constant) : void
    {
        list($namespace, $name) = $global_constant->toStubInfo();
        $this->global_constant_stubs[$namespace][(string)$global_constant->getFQSEN()] = $name;
    }

    public function addFunc(Func $function) : void
    {
        list($namespace, $name) = $function->toStubInfo();
        $this->function_stubs[$namespace][(string)$function->getFQSEN()] = $name;
    }

    /** @return string[][] */
    public function toCombinedStubs() : array
    {
        $result = [];
        foreach ($this->class_stubs as $namespace => $stubs) {
            ksort($stubs, SORT_NATURAL);
            $result[$namespace] = array_merge($result[$namespace] ?? [], $stubs);
        }
        foreach ($this->function_stubs as $namespace => $stubs) {
            ksort($stubs, SORT_NATURAL);
            $result[$namespace] = array_merge($result[$namespace] ?? [], $stubs);
        }
        foreach ($this->global_constant_stubs as $namespace => $stubs) {
            ksort($stubs, SORT_NATURAL);
            $result[$namespace] = array_merge($result[$namespace] ?? [], $stubs);
        }
        return $result;
    }

    /**
     * Returns the accumulated stubs converted to inline PHP code.
     */
    public function toString() : string
    {
        $parts = [];
        foreach ($this->toCombinedStubs() as $namespace => $stubs) {
            $concatenated_stubs_representation = implode('', $stubs);
            $namespace_repr = ($namespace === '' ? '' : "$namespace ");
            $parts[] = sprintf("namespace %s{\n%s}\n", $namespace_repr, $concatenated_stubs_representation);
        }
        return implode("\n", $parts);
    }
}
