<?php

declare(strict_types=1);

namespace Swoole\IDEHelper;

use GuzzleHttp\Client;
use ReflectionClass;
use ReflectionException;
use ReflectionExtension;
use ReflectionParameter;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use Swoole\IDEHelper\Rules\NamespaceRule;
use Symfony\Component\Filesystem\Filesystem;
use Laminas\Code\Generator\ClassGenerator;
use Laminas\Code\Generator\DocBlock\Tag\ReturnTag;
use Laminas\Code\Generator\DocBlockGenerator;
use Laminas\Code\Reflection\ClassReflection;

/**
 * Class AbstractStubGenerator
 *
 * @package Swoole\IDEHelper
 */
abstract class AbstractStubGenerator
{
    protected const C_METHOD   = 1;
    protected const C_PROPERTY = 2;
    protected const C_CONSTANT = 3;

    protected const DEFAULT_VERSION = "master";

    /**
     * @var string
     */
    protected $extension;

    /**
     * @var string
     */
    protected $language;

    /**
     * @var string
     */
    protected $dirConfig;

    /**
     * @var string
     */
    protected $dirOutput;

    /**
     * @var ReflectionExtension
     */
    protected $rf_ext;

    /**
     * @var string
     */
    protected $rf_version;

    protected const ALIAS_SHORT_NAME = 1; // Short names of coroutine classes.
    protected const ALIAS_SNAKE_CASE = 2; // Class names in snake_case. e.g., swoole_timer.

    protected $aliases = [
        self::ALIAS_SHORT_NAME => [],
        self::ALIAS_SNAKE_CASE => [],
    ];

    /**
     * Methods that don't need to have return type specified.
     */
    protected const IGNORED_METHODS = [
        '__construct' => null,
        '__destruct'  => null,
    ];

    /**
     * AbstractStubGenerator constructor.
     *
     * @throws Exception
     * @throws ReflectionException
     */
    public function __construct()
    {
        $this->init();

        if (!extension_loaded($this->extension)) {
            throw new Exception("Extension $this->extension not enabled or not installed.");
        }

        $this->language  = 'chinese';
        $this->dirOutput = dirname(__DIR__) . '/output/' . $this->extension;
        $this->dirConfig = dirname(__DIR__) . '/config';
        $this->rf_ext    = new ReflectionExtension($this->extension);
    }

    /**
     * @throws Exception
     * @throws ReflectionException
     */
    public function export(): void
    {
        // Retrieve and save all constants.
        if ($this->rf_ext->getConstants()) {
            $defines = '';
            foreach ($this->rf_ext->getConstants() as $name => $value) {
                $defines .= sprintf("define('%s', %s);\n", $name, (is_numeric($value) ? $value : "'{$value}'"));
            }
            $this->writeToPhpFile($this->dirOutput . '/constants.php', $defines);
        }

        // Retrieve and save all functions.
        $output = $this->getFunctionsDef();
        if (!empty($output)) {
            $this->writeToPhpFile($this->dirOutput . '/functions.php', $output);
        }

        // Retrieve and save all classes.
        $classes = $this->rf_ext->getClasses();
        // There are three types of class names in Swoole:
        // 1. short name of a class. Short names start with "Co\", and they can be found in file output/aliases.php.
        // 2. fully qualified name (class name with namespace prefix), e.g., \Swoole\Timer. These classes can be found
        //    under folder output/namespace.
        // 3. snake_case. e.g., swoole_timer. These aliases can be found in file output/aliases.php.
        foreach ($classes as $className => $ref) {
            if (strtolower(substr($className, 0, 3)) == 'co\\') {
                $className = str_replace('Swoole\\Coroutine', 'Co', $ref->getName());
                $this->aliases[self::ALIAS_SHORT_NAME][$className] = $ref->getName();
            } elseif (strchr($className, '\\')) {
                $this->exportNamespaceClass($className, $ref);
            } else {
                $this->aliases[self::ALIAS_SNAKE_CASE][$className] = $this->getNamespaceAlias($className);
            }
        }

        $class_alias = '';
        foreach (array_filter($this->aliases) as $type => $aliases) {
            if (!empty($class_alias)) {
                $class_alias .= "\n";
            }
            asort($aliases);
            foreach ($aliases as $alias => $original) {
                $class_alias .= "class_alias({$original}::class, {$alias}::class);\n";
            }
        }
        $this->writeToPhpFile($this->dirOutput . '/aliases.php', $class_alias);
    }

    /**
     * @return string
     */
    public function getVersion(): string
    {
        return $this->rf_ext->getVersion();
    }

    /**
     * @return string
     */
    public function getExtension(): string
    {
        return $this->extension;
    }

    /**
     * @param string $extension
     * @return $this
     */
    public function setExtension(string $extension): self
    {
        $this->extension = $extension;

        return $this;
    }

    /**
     * @param string $className
     * @return string
     */
    protected function getNamespaceAlias(string $className): string
    {
        if (strcasecmp($className, 'co') === 0) {
            return Coroutine::class;
        } elseif (strcasecmp($className, 'chan') === 0) {
            return Channel::class;
        } else {
            return str_replace('_', '\\', ucwords($className, '_'));
        }
    }

    /**
     * @param string $class
     * @param string $name
     * @param string $type
     * @return array
     */
    protected function getConfig(string $class, string $name, string $type): array
    {
        switch ($type) {
            case self::C_CONSTANT:
                $dir = 'constant';
                break;
            case self::C_METHOD:
                $dir = 'method';
                break;
            case self::C_PROPERTY:
                $dir = 'property';
                break;
            default:
                return false;
        }
        $file = $this->dirConfig . '/' . $this->language . '/' . strtolower($class) . '/' . $dir . '/' . $name . '.php';
        if (is_file($file)) {
            return include $file;
        } else {
            return array();
        }
    }

    /**
     * @param ReflectionParameter $parameter
     * @return string|null
     */
    protected function getDefaultValue(ReflectionParameter $parameter): ?string
    {
        try {
            $default_value = $parameter->getDefaultValue();
            if ($default_value === []) {
                $default_value = '[]';
            } elseif ($default_value === null) {
                $default_value = 'null';
            } elseif (is_bool($default_value)) {
                $default_value = $default_value ? 'true' : 'false';
            } else {
                $default_value = var_export($default_value, true);
            }
        } catch (\Throwable $e) {
            if ($parameter->isOptional()) {
                $default_value = 'null';
            } else {
                $default_value = null;
            }
        }
        return $default_value;
    }

    /**
     * @return string
     */
    protected function getFunctionsDef(): string
    {
        $all = '';
        foreach ($this->rf_ext->getFunctions() as $function) {
            $vp = array();
            $comment = "/**\n";
            $params = $function->getParameters();
            foreach ($params as $param) {
                $default_value = $this->getDefaultValue($param);
                $comment .= " * @param \${$param->name}[" .
                    ($param->isOptional() ? 'optional' : 'required') .
                    "]\n";
                $vp[] = ($param->isPassedByReference() ? '&' : '') .
                    "\${$param->name}" .
                    ($default_value ? " = {$default_value}" : '');
            }
            $comment .= " * @return mixed\n";
            $comment .= " */\n";
            $comment .= sprintf("function %s(%s){}\n\n", $function->getName(), join(', ', $vp));
            $all .= $comment;
        }

        return $all;
    }

    /**
     * @param string $classname
     * @param ReflectionClass $ref
     * @throws Exception
     * @throws ReflectionException
     */
    protected function exportNamespaceClass(string $classname, ReflectionClass $ref): void
    {
        (new NamespaceRule($this))->validate($classname);

        $class = ClassGenerator::fromReflection(new ClassReflection($ref->getName()));
        foreach ($class->getMethods() as $method) {
            if ((null === $method->getReturnType()) && !array_key_exists($method->getName(), self::IGNORED_METHODS)) {
                $method->setDocBlock(
                    DocBlockGenerator::fromArray(
                        [
                            'shortDescription' => null,
                            'longDescription'  => null,
                            'tags'             => [
                                new ReturnTag(
                                    [
                                        'datatype' => 'mixed',
                                    ]
                                ),
                            ],
                        ]
                    )
                );
            }
        }

        $this->writeToPhpFile(
            $this->dirOutput . '/namespace/' . implode('/', array_slice(explode('\\', $classname), 1)) . '.php',
            $class->generate()
        );
    }

    /**
     * @param string $path
     * @param string $content
     * @return AbstractStubGenerator
     */
    protected function writeToPhpFile(string $path, string $content): self
    {
        $this->mkdir(dirname($path));
        file_put_contents($path, "<?php\n\n" . $content);

        return $this;
    }

    /**
     * @param string $dir
     * @return AbstractStubGenerator
     */
    protected function mkdir(string $dir): self
    {
        if (!is_dir($dir)) {
            mkdir($dir, 0777, true);
        }

        return $this;
    }

    /**
     * @param string $name
     * @param string $version
     * @param string $targetDir
     * @return $this
     * @throws Exception
     */
    protected function download(string $name, string $version = self::DEFAULT_VERSION, string $targetDir = ""): self
    {
        $version   = $version ?: self::DEFAULT_VERSION;
        $targetDir = $targetDir ?: $name;

        if (preg_match("/^v[0-9]+\.[0-9]+\.[0-9]+(\-[A-Za-z0-9]+)?$/", $version)) {
            $downloadUrl = "https://github.com/swoole/${name}/archive/${version}.zip";
            $version     = substr($version, 1);
        } elseif (preg_match("/^[0-9]+\.[0-9]+\.[0-9]+(\-[A-Za-z0-9]+)?$/", $version)) {
            $downloadUrl = "https://github.com/swoole/${name}/archive/v${version}.zip";
        } else {
            $downloadUrl = "https://github.com/swoole/${name}/archive/${version}.zip";
        }
        $unzippedDir = "${name}-${version}";

        if (file_exists("temp.zip")) {
            unlink("temp.zip");
        }
        $this->deleteDir($unzippedDir)->deleteDir($targetDir);

        (new Client())->get($downloadUrl, ["sink" => "temp.zip"]);

        shell_exec("unzip temp.zip");
        if (!is_dir($unzippedDir)) {
            throw new Exception(
                sprintf(
                    "Top directory in the zip file downloaded from URL '%s' is not '%s'.",
                    $downloadUrl,
                    $unzippedDir
                )
            );
        }
        rename($unzippedDir, $targetDir);
        unlink("temp.zip");

        return $this;
    }

    /**
     * @param string $dir
     * @param bool $recreate
     * @throws Exception
     */
    protected function deleteDir(string $dir, bool $recreate = false): self
    {
        (new Filesystem())->remove($dir);
        if ($recreate) {
            $this->mkdir($dir);
        }

        return $this;
    }

    /**
     * @return AbstractStubGenerator
     */
    abstract protected function init(): AbstractStubGenerator;
}