<?php

declare(strict_types=1);

namespace CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator;

use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Description, Example, FunctionParameter, FunctionReturn, PublicAPI, Related, Throws};
use HaydenPierce\ClassFinder\ClassFinder;

class Generate
{
    // Static - Translators

    public static function makeFilename(\ReflectionMethod $method): string
    {
        return self::getModifiersName($method).
                ' '.
                str_replace('\\', '_', self::simpleClass($method->class)).'--'.$method->name.
                '.md';
    }

    public static function simpleClass(string $fullClassName): string
    {
        return str_replace('CondorcetPHP\\Condorcet\\', '', $fullClassName);
    }

    public static function speakBool($c): string
    {
        if ($c === true || $c === 'true') {
            return 'true';
        }
        if ($c === false || $c === 'false') {
            return 'false';
        }
        if ($c === null || $c === 'null') {
            return 'null';
        }
        if (\is_array($c)) {
            return '['.implode(',', $c).']';
        }
        if (\is_object($c)) {
            return 'new '.$c::class;
        }

        return (string) $c;
    }

    public static function getTypeAsString(?\ReflectionType $rf_rt, bool $codeBlock = false): ?string
    {
        if ($rf_rt !== null) {
            if ($codeBlock) {
                return '```'.((string) $rf_rt).'```';
            } else {
                return (string) $rf_rt;
            }
        }

        return $rf_rt;
    }

    public static function getModifiersName(\ReflectionMethod $method): string
    {
        return implode(' ', \Reflection::getModifierNames($method->getModifiers()));
    }

    // Static - Builder

    public static function cleverRelated(string $name): string
    {
        $infos = explode('::', $name);
        $infos[0] = str_replace('static ', '', $infos[0]);

        $url = '../'.$infos[0].' Class/public '.str_replace('::', '--', $name) . '.md';
        $url = str_replace(' ', '%20', $url);

        return '['.$name.']('.$url.')';
    }

    public static function computeRepresentationAsForIndex(\ReflectionMethod $method): string
    {
        return self::getModifiersName($method).
                ' '.
                self::simpleClass($method->class).
                (($method->isStatic()) ? '::' : '->').
                $method->name.
                ' ('.(($method->getNumberOfParameters() > 0) ? '...' : '').')';
    }

    public static function computeRepresentationAsPHP(\ReflectionMethod $method): string
    {
        $option = false;
        $str = '(';
        $i = 0;


        if ($method->getNumberOfParameters() > 0) {
            foreach ($method->getParameters() as $value) {
                $str .= ' ';
                $str .= ($value->isOptional() && !$option) ? '[' : '';
                $str .= ($i > 0) ? ', ' : '';
                $str .= self::getTypeAsString($value->getType());
                $str .= ' ';
                $str .= $value->isPassedByReference() ? '&' : '';
                $str .= '$'.$value->getName();
                $str .= $value->isDefaultValueAvailable() ? ' = '.self::speakBool($value->getDefaultValue()) : '';

                ($value->isOptional() && !$option) ? $option = true : null;
                $i++;
            }
        }

        if ($option) {
            $str .= ']';
        }

        $str .= ' )';

        return "```php\n".
                self::getModifiersName($method).' '.self::simpleClass($method->class).(($method->isStatic()) ? '::' : '->').$method->name.' '.$str. ((self::getTypeAsString($method->getReturnType()) !== null) ? ': '.self::getTypeAsString($method->getReturnType()) : '').
                "\n```";
    }


    // Script
    public function __construct(string $path)
    {
        $start_time = microtime(true);

        $pathDirectory = $path.\DIRECTORY_SEPARATOR;

        //
        $FullClassList = ClassFinder::getClassesInNamespace('CondorcetPHP\Condorcet\\', ClassFinder::RECURSIVE_MODE);
        $FullClassList = array_filter($FullClassList, static function (string $value) {
            return (mb_strpos($value, 'Condorcet\Test') === false) && (mb_strpos($value, 'Condorcet\Dev') === false);
        });

        $inDoc = 0;
        $non_inDoc = 0;
        $total_methods = 0;
        $total_nonInternal_methods = 0;

        // Warnings
        foreach ($FullClassList as $FullClass) {
            $methods = (new \ReflectionClass($FullClass))->getMethods(\ReflectionMethod::IS_PUBLIC);

            foreach ($methods as $oneMethod) {
                if ($oneMethod->isInternal()) {
                } elseif (!empty($oneMethod->getAttributes(PublicAPI::class))) {
                    $inDoc++;

                    if ($oneMethod->getNumberOfParameters() > 0) {
                        foreach ($oneMethod->getParameters() as $oneParameter) {
                            if (empty($oneParameter->getAttributes(FunctionParameter::class))) {
                                var_dump('Method Has Public API attribute but parameter $'.$oneParameter->getName().' is undocumented '.$oneMethod->getDeclaringClass()->getName().'->'.$oneMethod->getName());
                            }
                        }
                    }

                    if (empty($oneMethod->getAttributes(Description::class)) && $oneMethod->getDeclaringClass()->getNamespaceName() !== '') {
                        var_dump('Description Attribute is empty: '.$oneMethod->getDeclaringClass()->getName().'->'.$oneMethod->getName());
                    }
                } else {
                    $non_inDoc++;

                    if (empty($oneMethod->getAttributes(PublicAPI::class)) && $oneMethod->getDeclaringClass()->getNamespaceName() !== '') {
                        // var_dump('Method not has API attribute: '.$oneMethod->getDeclaringClass()->getName().'->'.$oneMethod->getName());
                    }
                }
            }
        }

        $full_methods_list = [];

        // generate .md
        foreach ($FullClassList as $FullClass) {
            $reflectionClass = new \ReflectionClass($FullClass);
            $methods = ($reflectionClass)->getMethods();
            $shortClass = str_replace('CondorcetPHP\Condorcet\\', '', $FullClass);

            $full_methods_list[$shortClass] = [
                'FullClass' => $FullClass,
                'shortClass' => $shortClass,
                'ReflectionClass' => $reflectionClass,
                'methods' => [],
            ];

            foreach ($methods as $oneMethod) {
                $method_array = $full_methods_list[$shortClass]['methods'][$oneMethod->name] = [
                    'FullClass' => $FullClass,
                    'shortClass' => $shortClass,
                    'name' => $oneMethod->name,
                    'static' => $oneMethod->isStatic(),
                    'visibility_public' => $oneMethod->isPublic(),
                    'visibility_protected' => $oneMethod->isProtected(),
                    'visibility_private' => $oneMethod->isPrivate(),
                    'ReflectionMethod' => $oneMethod,
                    'ReflectionClass' => $oneMethod->getDeclaringClass(),
                ];

                $total_methods++;

                if (!$oneMethod->isInternal()) {
                    $total_nonInternal_methods++;
                }

                // Write Markdown
                if (!empty($apiAttribute = $oneMethod->getAttributes(PublicAPI::class)) && (empty($apiAttribute[0]->getArguments()) || \in_array(self::simpleClass($oneMethod->class), $apiAttribute[0]->getArguments(), true))) {
                    $path = $pathDirectory . str_replace('\\', '_', self::simpleClass($oneMethod->class)) . ' Class/';

                    if (!is_dir($path)) {
                        mkdir($path);
                    }

                    file_put_contents($path.self::makeFilename($oneMethod), $this->createMarkdownContent($oneMethod, $method_array));
                }
            }
        }


        print 'Public methods in doc: '.$inDoc.' / '.($inDoc + $non_inDoc).' | Total non-internal methods count: '.$total_nonInternal_methods.' | Number of Class: '.\count($FullClassList).' | Number of Methods including internals: '.$total_methods."\n";

        // Add Index
        $file_content =  "> **[Presentation](../README.md) | [Manual](https://github.com/julien-boudry/Condorcet/wiki) | Methods References | [Tests](../Tests)**\n\n".

                        "# Public API Index*_\n".

                        "_*: I try to update and complete the documentation. See also [the manual](https://github.com/julien-boudry/Condorcet/wiki), [the tests](../Tests) also produce many examples. And create issues for questions or fixing documentation!_\n\n";


        $file_content .= $this->makeIndex($full_methods_list);

        $file_content .= "\n\n\n";

        uksort($full_methods_list, 'strnatcmp');
        $file_content .=    "# Full Class & Methods References\n".
                            "_Including above methods from public API_\n\n";

        $file_content .= $this->makeProfundis($full_methods_list);


        // Write file
        file_put_contents($pathDirectory.'README.md', $file_content);


        echo 'YAH ! <br>' . (microtime(true) - $start_time) .'s';
    }


    // Build Methods

    protected function createMarkdownContent(\ReflectionMethod $method, array $entry): string
    {
        // Header
        $md =   '## '.self::getModifiersName($method).' '. self::simpleClass($method->class).'::'.$method->name."\n\n".
                "### Description    \n\n".
                self::computeRepresentationAsPHP($method)."\n\n".
                $method->getAttributes(Description::class)[0]->getArguments()[0]."\n    ";

        // Input
        if ($method->getNumberOfParameters() > 0) {
            foreach ($method->getParameters() as $key => $value) {
                if (!empty($attributes = $value->getAttributes(FunctionParameter::class))) {
                    $pt = $attributes[0]->newInstance()->text;
                } elseif (isset($entry['input'][$value->getName()]['text'])) {
                    $pt = $entry['input'][$value->getName()]['text'];
                } else {
                    $pt = '';
                }

                $md .=  "\n\n".
                        '##### **'.$value->getName().':** *'.self::getTypeAsString($value->getType(), true)."*   \n".
                        $pt."    \n";
            }
        }


        // Return Value

        if (!empty($method->getAttributes(FunctionReturn::class))) {
            $md .= "\n\n".
                    "### Return value:   \n\n".
                    '*('.self::getTypeAsString($method->getReturnType(), true).')* '.$method->getAttributes(FunctionReturn::class)[0]->getArguments()[0]."\n\n";
        }

        // Throw
        if (!empty($method->getAttributes(Throws::class))) {
            $md .=  "\n\n".
                    "### Throws:   \n\n";

            foreach ($method->getAttributes(Throws::class)[0]->getArguments() as $arg) {
                $md .= '* ```'.$arg."```\n";
            }
        }

        // Related methods

        if (!empty($method->getAttributes(Related::class))) {
            $md .=  "\n".
                    "---------------------------------------\n\n".
                    "### Related method(s)      \n\n";

            foreach ($method->getAttributes(Related::class) as $RelatedAttribute) {
                foreach ($RelatedAttribute->newInstance()->relatedList as $value) {
                    if ($value === self::simpleClass($method->class).'::'.$method->name) {
                        continue;
                    }

                    $md .= '* '.self::cleverRelated($value)."    \n";
                }
            }
        }

        if (!empty($method->getAttributes(Example::class))) {
            $md .=  "\n".
                    "---------------------------------------\n\n".
                    "### Examples and explanation\n\n";

            foreach ($method->getAttributes(Example::class) as $ExampleAttribute) {
                $ExampleAttribute = $ExampleAttribute->newInstance();

                $md .= '* **['.$ExampleAttribute->name.']('.$ExampleAttribute->link.")**    \n";
            }
        }

        return $md;
    }

    protected function makeIndex(array $index): string
    {
        $file_content = '';

        $testPublicAttribute = static function (\ReflectionMethod $reflectionMethod): bool {
            return !(empty($apiAttribute = $reflectionMethod->getAttributes(PublicAPI::class)) || (!empty($apiAttribute[0]->getArguments()) && !\in_array(self::simpleClass($reflectionMethod->class), $apiAttribute[0]->getArguments(), true)));
        };

        foreach ($index as $class => &$classMeta) {
            usort($classMeta['methods'], static function (array $a, array $b): int {
                if ($a['ReflectionMethod']->isStatic() === $b['ReflectionMethod']->isStatic()) {
                    return strnatcmp($a['ReflectionMethod']->name, $b['ReflectionMethod']->name);
                } elseif ($a['ReflectionMethod']->isStatic() && !$b['ReflectionMethod']->isStatic()) {
                    return -1;
                } else {
                    return 1;
                }
            });

            $classWillBePublic = false;

            if ($classMeta['ReflectionClass']->getAttributes(PublicAPI::class)) {
                $classWillBePublic = true;
            } else {
                foreach ($classMeta['methods'] as $oneMethod) {
                    if ($testPublicAttribute($oneMethod['ReflectionMethod'])) {
                        $classWillBePublic = true;
                        break;
                    }
                }

                foreach ($classMeta['ReflectionClass']->getReflectionConstants() as $oneConstant) {
                    if (!empty($oneConstant->getAttributes(PublicAPI::class))) {
                        $classWillBePublic = true;
                        break;
                    }
                }

                foreach ($classMeta['ReflectionClass']->getProperties() as $onePropertie) {
                    if (!empty($onePropertie->getAttributes(PublicAPI::class))) {
                        $classWillBePublic = true;
                        break;
                    }
                }
            }

            if ($classWillBePublic) {
                $isEnum = enum_exists(($enumCases = $classMeta['ReflectionClass'])->name);

                $file_content .= "\n";
                $file_content .= '### CondorcetPHP\Condorcet\\'.$class.' '.((!$isEnum) ? 'Class' : 'Enum')."  \n\n";

                if ($isEnum) {
                    $file_content .= $this->makeEnumeCases(new \ReflectionEnum($enumCases->name), false);
                    $file_content .= "\n";
                } else {
                    $file_content .= $this->makeConstants($classMeta['ReflectionClass'], \ReflectionClassConstant::IS_PUBLIC, true);
                }

                $file_content .= $this->makeProperties($classMeta['ReflectionClass'], null, true);
            }


            foreach ($classMeta['methods'] as $oneMethod) {
                if (!$testPublicAttribute($oneMethod['ReflectionMethod']) || !$oneMethod['ReflectionMethod']->isUserDefined()) {
                    continue;
                } else {
                    $url = str_replace('\\', '_', self::simpleClass($oneMethod['ReflectionMethod']->class)).' Class/'.self::getModifiersName($oneMethod['ReflectionMethod']).' '. str_replace('\\', '_', self::simpleClass($oneMethod['ReflectionMethod']->class).'--'. $oneMethod['ReflectionMethod']->name) . '.md';
                    $url = str_replace(' ', '%20', $url);

                    $file_content .= '* ['.self::computeRepresentationAsForIndex($oneMethod['ReflectionMethod']).']('.$url.')';

                    if (isset($oneMethod['ReflectionMethod']) && $oneMethod['ReflectionMethod']->hasReturnType()) {
                        $file_content .= ': '.self::getTypeAsString($oneMethod['ReflectionMethod']->getReturnType(), true);
                    }

                    $file_content .= "  \n";
                }
            }
        }

        return $file_content;
    }

    protected function makeEnumeCases(\ReflectionEnum $enumReflection, bool $shortName = false): string
    {
        $cases = $enumReflection->getCases();

        $r = '';

        foreach ($cases as $oneCase) {
            $name = ($shortName) ? $enumReflection->getShortName() : self::simpleClass($enumReflection->getName());
            $r .= '* ```case '.$name.'::'.$oneCase->getName()."```  \n";
        }

        return $r;
    }

    protected function makeConstants(\ReflectionClass $class, ?int $type = null, bool $mustHaveApiAttribute = false, bool $addMdCodeTag = true): string
    {
        $file_content = '';

        $hasConstants = false;

        foreach ($class->getReflectionConstants($type) as $constant) {
            if (!$mustHaveApiAttribute || !empty($constant->getAttributes(PublicAPI::class))) {
                $file_content .= '* ';
                $file_content .=  $addMdCodeTag ? '```' : '';

                $file_content .= $constant->isFinal() ? 'final ' : '';

                $file_content .= $constant->isPublic() ? 'public' : '';
                $file_content .= $constant->isProtected() ? 'protected' : '';
                $file_content .= $constant->isPrivate() ? 'private' : '';

                $file_content .= ' const '.$constant->getName().': ('.\gettype($constant->getValue()).')';
                $file_content .= $addMdCodeTag ? '```  ' : '';
                $file_content .= "\n";
                $hasConstants = true;
            }
        }

        if ($hasConstants) {
            $file_content .= "\n";
        }

        return $file_content;
    }

    protected function makeProperties(\ReflectionClass $class, ?int $type = null, bool $mustHaveApiAttribute = false, bool $addMdCodeTag = true): string
    {
        $file_content = '';

        $hasConstants = false;

        foreach ($class->getProperties($type) as $propertie) {
            if (!$mustHaveApiAttribute || !empty($propertie->getAttributes(PublicAPI::class))) {
                $file_content .= '* ';
                $file_content .=  $addMdCodeTag ? '```' : '';

                $file_content .= $propertie->isReadOnly() ? 'readonly ' : '';

                $file_content .= $propertie->isPublic() ? 'public ' : '';
                $file_content .= $propertie->isProtected() ? 'protected ' : '';
                $file_content .= $propertie->isPrivate() ? 'private ' : '';

                $file_content .= $propertie->isStatic() ? 'static ' : '';

                $file_content .= ((string) $propertie->getType()).' $'.$propertie->getName();
                $file_content .= $addMdCodeTag ? '```  ' : '';
                $file_content .= "\n";
                $hasConstants = true;
            }
        }

        if ($hasConstants) {
            $file_content .= "\n";
        }

        return $file_content;
    }

    protected function makeProfundis(array $index): string
    {
        $file_content = '';

        foreach ($index as $class => &$classMeta) {
            usort($classMeta['methods'], static function (array $a, array $b): int {
                if ($a['static'] === $b['static']) {
                    if ($a['visibility_public'] && !$b['visibility_public']) {
                        return -1;
                    } elseif (!$a['visibility_public'] && $b['visibility_public']) {
                        return 1;
                    } else {
                        if ($a['visibility_protected'] && !$b['visibility_protected']) {
                            return -1;
                        } elseif (!$a['visibility_protected'] && $b['visibility_protected']) {
                            return 1;
                        } else {
                            return strnatcmp($a['name'], $b['name']);
                        }
                    }
                } elseif ($a['static'] && !$b['static']) {
                    return -1;
                } else {
                    return 1;
                }
            });

            $file_content .= "\n";
            $file_content .= '#### ';
            $file_content .= ($classMeta['ReflectionClass']->isAbstract()) ? 'Abstract ' : '';
            $file_content .= 'CondorcetPHP\Condorcet\\'.$class.' ';

            $file_content .= ($p = $classMeta['ReflectionClass']->getParentClass()) ? 'extends '.$p->name.' ' : '';

            $interfaces = implode(', ', $classMeta['ReflectionClass']->getInterfaceNames());
            $file_content .= (!empty($interfaces)) ? 'implements '.$interfaces : '';

            $file_content .= "  \n";
            $file_content .= "```php\n";

            $isEnum = enum_exists(($enumCases = $classMeta['ReflectionClass'])->name);

            if ($isEnum) {
                $file_content .= $this->makeEnumeCases(new \ReflectionEnum($enumCases->name), true);
                $file_content .= "\n";
            } else {
                $file_content .= $this->makeConstants(class: $classMeta['ReflectionClass'], addMdCodeTag: false);
            }

            $file_content .= $this->makeProperties(class: $classMeta['ReflectionClass'], addMdCodeTag: false);

            foreach ($classMeta['methods'] as $oneMethod) {
                if ($oneMethod['ReflectionMethod']->isUserDefined()) {
                    $parameters = $oneMethod['ReflectionMethod']->getParameters();
                    $parameters_string = '';

                    $i = 0;
                    foreach ($parameters as $oneP) {
                        $parameters_string .= (++$i > 1) ? ', ' : '';

                        if ($oneP->getType() !== null) {
                            $parameters_string .= self::getTypeAsString($oneP->getType()) . ' ';
                        }
                        $parameters_string .= '$'.$oneP->name;

                        if ($oneP->isDefaultValueAvailable()) {
                            $parameters_string .= ' = '.self::speakBool($oneP->getDefaultValue());
                        }
                    }

                    $representation = ($oneMethod['visibility_public']) ? 'public ' : '';
                    $representation .= ($oneMethod['visibility_protected']) ? 'protected ' : '';
                    $representation .= ($oneMethod['visibility_private']) ? 'private ' : '';

                    $representation .=  ($oneMethod['static']) ? 'static ' : '';
                    $representation .=  $oneMethod['name'] . ' ('.$parameters_string.')';

                    if ($oneMethod['ReflectionMethod']->hasReturnType()) {
                        $representation .= ': '.self::getTypeAsString($oneMethod['ReflectionMethod']->getReturnType());
                    }

                    $file_content .= '* '.$representation."  \n";
                }
            }

            $file_content .= "```\n";
        }

        return $file_content;
    }
}
