#!/usr/bin/env php
<?php

/**
 * 决定是否忽略该类
 *
 * @param ReflectionClass $class 准备生成的类的反射类
 */
function should_ignore_class(ReflectionClass $class): bool
{
    // 注解类
    if (($doc = $class->getDocComment()) && strpos($doc, '@Annotation') !== false) {
        return true;
    }
    if (str_ends_with($class->getName(), 'Exception')) {
        return true;
    }
    if (str_contains($class->getName(), '_')) {
        return true;
    }
    return false;
}

if (PHP_VERSION_ID < 70400) {
    echo 'PHP 版本必须为 7.4 或以上';
    exit(1);
}

function check_composer_executable(string $composer): bool
{
    passthru($composer . ' --version 2>/dev/null', $exit_code);
    return $exit_code === 0;
}

function find_valid_root(string $current_dir, int $max_loop): string
{
    if ($max_loop <= 0) {
        return '';
    }
    if (!file_exists($current_dir . '/composer.json')) {
        return find_valid_root(dirname($current_dir), $max_loop - 1);
    }
    return $current_dir;
}

$root = find_valid_root(getcwd(), 3);
if (empty($root)) {
    echo '找不到有效的根目录';
    exit(1);
}
echo '根目录: ' . $root . PHP_EOL;

chdir($root);

if (!is_dir($root . '/src/ZM')) {
    echo '源码目录不存在';
    exit(1);
}

$phpdoc_package_temp_installed = false;
if (file_exists($root . '/vendor/jasny/phpdoc-parser/composer.json')) {
    echo '正在使用现有的 PHPDoc 解析库' . PHP_EOL;
} else {
    echo 'PHPDoc 解析库不存在，正在尝试安装...' . PHP_EOL;
    echo '本次生成完成后将会自动移除' . PHP_EOL;
    $composers = [
        'runtime/composer', 'composer', 'php composer.phar',
    ];
    foreach ($composers as $composer) {
        if (check_composer_executable($composer)) {
            echo '正在使用 ' . $composer . ' 安装 PHPDoc 解析库，请稍候...' . PHP_EOL;
            passthru("{$composer} require --quiet jasny/phpdoc-parser", $exit_code);
            if ($exit_code === 0) {
                $phpdoc_package_temp_installed = true;
                break;
            }
        }
    }
    if (!$phpdoc_package_temp_installed) {
        echo '安装失败，请手动安装：composer require jasny/phpdoc-parser' . PHP_EOL;
        exit(1);
    }
}

// 从这里开始是主要生成逻辑

require_once $root . '/vendor/autoload.php';

use Jasny\PhpdocParser\PhpdocParser;
use Jasny\PhpdocParser\Set\PhpDocumentor;
use Jasny\PhpdocParser\Tag\Summery;
use ZM\Console\Console;

$opts = getopt('', ['show-warnings', 'show-debug']);

if (array_key_exists('show-warnings', $opts)) {
    $show_warnings = true;
} else {
    $show_warnings = false;
}

Console::init(array_key_exists('show-debug', $opts) ? 4 : 2);

$errors = [];
$warnings = [];

// 获取源码目录的文件遍历器
$fs = new \RecursiveDirectoryIterator($root . '/src/ZM', FilesystemIterator::SKIP_DOTS);

// 初始化文档解析器
$parser = new PhpdocParser(PhpDocumentor::tags()->with([
    new Summery(),
]));

$metas = [];
$class_count = 0;
$method_count = 0;

function error(string $message)
{
    global $errors;
    $errors[] = $message;
    Console::error($message);
}

function warning(string $message)
{
    global $warnings, $show_warnings;
    $warnings[] = $message;
    if ($show_warnings) {
        Console::warning($message);
    }
}

/**
 * 获取类的元数据
 *
 * 包括类的注释、方法的注释、参数、返回值等
 */
function get_class_metas(string $class_name, PhpdocParser $parser): array
{
    // 尝试获取反射类
    try {
        $class = new \ReflectionClass($class_name);
    } catch (\ReflectionException $e) {
        error('无法获取类 ' . $class_name . ' 的反射类');
        return [];
    }

    // 判断是否略过该类
    if (should_ignore_class($class)) {
        return [];
    }

    $metas = [];

    // 遍历类方法
    foreach ($class->getMethods() as $method) {
        if ($method->getDeclaringClass()->getName() !== $class_name) {
            continue;
        }

        Console::verbose('正在解析方法：' . $class_name . '::' . $method->getName());

        // 获取方法的注释并解析
        $doc = $method->getDocComment();
        if (!$doc) {
            warning('找不到文档：' . $class_name . '::' . $method->getName());
            continue;
        }
        try {
            $meta = $parser->parse($doc);
        } catch (\Exception $e) {
            error('解析失败：' . $class_name . '::' . $method->getName() . '，' . $e->getMessage());
            continue;
        }
        // 少数情况解析后会带有 */，需要去除
        array_walk_recursive($meta, static function (&$item) {
            if (is_string($item)) {
                $item = trim(str_replace('*/', '', $item));
            }
        });

        // 对比反射方法获取的参数和注释声明的参数
        $parameters = $method->getParameters();
        $params_in_doc = $meta['params'] ?? [];

        foreach ($parameters as $parameter) {
            $parameter_name = $parameter->getName();
            // 不存在则添加进参数列表中
            if (!isset($params_in_doc[$parameter_name])) {
                $params_in_doc[$parameter_name] = [
                    'type' => $parameter->getType()?->getName(),
                    'description' => '',
                ];
            }
        }
        // 确保所有参数都有对应的类型和描述
        foreach ($params_in_doc as &$param) {
            if (!isset($param['type'])) {
                $param['type'] = 'mixed';
            }
            if (!isset($param['description'])) {
                $param['description'] = '';
            }
        }
        // 清除引用
        unset($param);
        $meta['params'] = $params_in_doc;

        // 设定方法默认返回值
        if (!isset($meta['return'])) {
            $meta['return'] = [
                'type' => $method->getReturnType()?->getName() ?: 'mixed',
                'description' => '',
            ];
        }

        // 设定默认描述
        if (!isset($meta['return']['description'])) {
            $meta['return']['description'] = '';
        }

        $metas[$method->getName()] = $meta;
    }

    return $metas;
}

/**
 * 将方法的元数据转换为 Markdown 格式
 *
 * @param string $method 方法名
 * @param array  $meta   元数据
 */
function convert_meta_to_markdown(string $method, array $meta): string
{
    // 方法名作为标题
    $markdown = '## ' . $method . "\n\n";

    // 构造方法代码块
    $markdown .= '```php' . "\n";
    // TODO: 适配 private 等修饰符
    $markdown .= 'public function ' . $method . '(';
    $params = [];
    // 添加参数
    foreach ($meta['params'] as $param_name => $param_meta) {
        $params[] = sprintf('%s $%s', $param_meta['type'] ?? 'mixed', $param_name);
    }
    $markdown .= implode(', ', $params) . ')';
    // 添加返回值
    $markdown .= ': ' . $meta['return']['type'];
    $markdown .= "\n```\n\n";

    // 方法描述
    $markdown .= '### 描述' . "\n\n";
    $markdown .= ($meta['description'] ?? '作者很懒，什么也没有说') . "\n\n";

    // 参数
    if (count($meta['params'])) {
        $markdown .= '### 参数' . "\n\n";
        $markdown .= '| 名称 | 类型 | 描述 |' . "\n";
        $markdown .= '| -------- | ---- | ----------- |' . "\n";
        foreach ($meta['params'] as $param_name => $param_meta) {
            $markdown .= '| ' . $param_name . ' | ' . $param_meta['type'] . ' | ' . $param_meta['description'] . ' |' . "\n";
        }
        $markdown .= "\n";
    }

    // 返回值
    $markdown .= '### 返回' . "\n\n";
    $markdown .= '| 类型 | 描述 |' . "\n";
    $markdown .= '| ---- | ----------- |' . "\n";
    $markdown .= '| ' . $meta['return']['type'] . ' | ' . $meta['return']['description'] . ' |' . "\n";

    return $markdown;
}

function generate_sidebar_config(string $title, array $items): array
{
    return [
        'title' => $title,
        'collapsable' => true,
        'children' => $items,
    ];
}

Console::info('开始生成！');

// 遍历类并将元数据添加至数组中
foreach (new \RecursiveIteratorIterator($fs) as $file) {
    if (!$file->isFile()) {
        continue;
    }
    $path = $file->getPathname();

    // 过滤不包含类的文件
    $tokens = token_get_all(file_get_contents($path));
    $found = false;
    foreach ($tokens as $k => $token) {
        if (!is_array($token)) {
            continue;
        }
        if ($token[0] === T_CLASS && $tokens[$k - 2][0] !== T_NEW) {
            $found = true;
            break;
        }
        if ($k >= 100) {
            break;
        }
    }
    if (!$found) {
        continue;
    }

    // 获取完整类名
    $path = ltrim($path, $root . '/');
    $class = str_replace(['.php', 'src/', '/'], ['', '', '\\'], $path);
    Console::verbose('正在解析类：' . $class);
    $meta = get_class_metas($class, $parser);
    // 忽略不包含任何方法的类
    if (empty($meta)) {
        continue;
    }
    $metas[$class] = $meta;
    ++$class_count;
    $method_count += count($meta);
}

// 将元数据转换为 Markdown
$markdown = [];
foreach ($metas as $class => $class_metas) {
    $markdown[$class] = [];
    // 将类名作为页面大标题
    $markdown[$class]['class'] = '# ' . $class;
    foreach ($class_metas as $method => $meta) {
        $markdown[$class][$method] = convert_meta_to_markdown($method, $meta);
    }
}

// 生成文档
$docs = $root . '/docs/api/';
foreach ($markdown as $class => $methods) {
    $file = $docs . str_replace('\\', '/', $class) . '.md';
    // 确保目录存在
    if (!file_exists(dirname($file)) && !mkdir($concurrent_directory = dirname($file), 0777, true) && !is_dir($concurrent_directory)) {
        throw new \RuntimeException(sprintf('无法建立目录 %s', $concurrent_directory));
    }
    Console::verbose('正在生成文档：' . $file);
    $text = implode("\n\n", $methods);
    file_put_contents($file, $text);
}

// 生成文档目录
$children = array_keys($markdown);
$children = str_replace('\\', '/', $children);
$class_tree = [];
foreach ($children as $child) {
    $parent = dirname($child);
    $class_tree[$parent][] = $child;
}
ksort($class_tree);
$config = 'module.exports = [';
foreach ($class_tree as $parent => $children) {
    $encode = json_encode(generate_sidebar_config($parent, $children));
    $encode = str_replace('\/', '/', $encode);
    $config .= $encode . ',';
}
$config = rtrim($config, ',');
$config .= ']';

$file = $root . '/docs/.vuepress/api.js';
file_put_contents($file, $config);

if (count($warnings)) {
    Console::warning('生成过程中发现 ' . count($warnings) . ' 次警告');
    if (!$show_warnings) {
        Console::info('生成器默认忽略了警告，你可以通过 gendoc --show-warnings 来查看警告');
    }
}
if (count($errors)) {
    Console::error('生成过程中发现错误：');
    foreach ($errors as $error) {
        Console::error($error);
    }
}

Console::success('API 文档生成完毕');
Console::success(sprintf('共生成 %d 个类，共 %d 个方法', $class_count, $method_count));

// 完成生成，进行善后

if ($phpdoc_package_temp_installed) {
    echo '正在移除 PHPDoc 解析库，请稍候...' . PHP_EOL;
    passthru('composer remove --quiet jasny/phpdoc-parser', $exit_code);
    if ($exit_code !== 0) {
        echo '移除失败，请手动移除：composer remove jasny/phpdoc-parser' . PHP_EOL;
    }
    echo '移除完成';
}
exit(0);
