<?php declare(strict_types=1);

namespace Psalm\Internal\Stubs\Generator;

use PhpParser;
use Psalm\Codebase;
use Psalm\Internal\Codebase\ConstantTypeResolver;
use Psalm\Node\Name\VirtualFullyQualified;
use Psalm\Node\Stmt\VirtualClass;
use Psalm\Node\Stmt\VirtualClassConst;
use Psalm\Node\Stmt\VirtualClassMethod;
use Psalm\Node\Stmt\VirtualInterface;
use Psalm\Node\Stmt\VirtualProperty;
use Psalm\Node\Stmt\VirtualTrait;
use Psalm\Node\VirtualConst;
use Psalm\Node\VirtualPropertyItem;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Scanner\ParsedDocblock;
use Psalm\Type;
use Psalm\Type\Union;

use ReflectionProperty;
use UnexpectedValueException;
use function array_slice;
use function rtrim;

/**
 * @internal
 */
final class ClassLikeStubGenerator
{
    /**
     * @return PhpParser\Node\Stmt\Class_|PhpParser\Node\Stmt\Interface_|PhpParser\Node\Stmt\Trait_
     */
    public static function getClassLikeNode(
        Codebase $codebase,
        ClassLikeStorage $storage,
        string $classlike_name
    ) : PhpParser\Node\Stmt\ClassLike {
        $subnodes = [
            'stmts' => [...self::getConstantNodes($codebase, $storage), ...self::getPropertyNodes($storage), ...self::getMethodNodes($storage)]
        ];

        $docblock = new ParsedDocblock('', []);

        $template_offset = 0;

        foreach ($storage->template_types ?: [] as $template_name => $map) {
            $type = array_values($map)[0];

            $key = isset($storage->template_covariants[$template_offset]) ? 'template-covariant' : 'template';

            $docblock->tags[$key][] = $template_name . ' as ' . $type->toNamespacedString(
                null,
                [],
                null,
                false
            );

            $template_offset++;
        }

        $attrs = [
            'comments' => $docblock->tags
                ? [
                    new PhpParser\Comment\Doc(
                        rtrim($docblock->render('        '))
                    )
                ]
                : []
        ];

        if ($storage->is_interface) {
            if ($storage->direct_interface_parents) {
                $subnodes['extends'] = [];

                foreach ($storage->direct_interface_parents as $direct_interface_parent) {
                    $subnodes['extends'][] = new VirtualFullyQualified($direct_interface_parent);
                }
            }

            return new VirtualInterface(
                $classlike_name,
                $subnodes,
                $attrs
            );
        }

        if ($storage->is_trait) {
            return new VirtualTrait(
                $classlike_name,
                $subnodes,
                $attrs
            );
        }

        if ($storage->parent_class) {
            $subnodes['extends'] = new VirtualFullyQualified($storage->parent_class);
        } else

        if ($storage->direct_class_interfaces) {
            $subnodes['implements'] = [];
            foreach ($storage->direct_class_interfaces as $direct_class_interface) {
                $subnodes['implements'][] = new VirtualFullyQualified($direct_class_interface);
            }
        }

        return new VirtualClass(
            $classlike_name,
            $subnodes,
            $attrs
        );
    }

    /**
     * @return list<PhpParser\Node\Stmt\ClassConst>
     */
    private static function getConstantNodes(Codebase $codebase, ClassLikeStorage $storage): array
    {
        $constant_nodes = [];

        foreach ($storage->constants as $constant_name => $constant_storage) {
            if ($constant_storage->unresolved_node) {
                $type = new Union([
                    ConstantTypeResolver::resolve(
                        $codebase->classlikes,
                        $constant_storage->unresolved_node
                    )
                ]);
            } elseif ($constant_storage->type) {
                $type = $constant_storage->type;
            } else {
                throw new UnexpectedValueException('bad');
            }

            $constant_nodes[] = new VirtualClassConst(
                [
                    new VirtualConst(
                        $constant_name,
                        StubsGenerator::getExpressionFromType($type)
                    )
                ],
                $constant_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
                    ? PhpParser\Modifiers::PUBLIC
                    : ($constant_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED
                        ? PhpParser\Modifiers::PROTECTED
                        : PhpParser\Modifiers::PRIVATE)
            );
        }

        return $constant_nodes;
    }

    /**
     * @return list<PhpParser\Node\Stmt\Property>
     */
    private static function getPropertyNodes(ClassLikeStorage $storage): array
    {
        $namespace_name = implode('\\', array_slice(explode('\\', $storage->name), 0, -1));

        $property_nodes = [];

        foreach ($storage->properties as $property_name => $property_storage) {
            $flag = match ($property_storage->visibility) {
                ClassLikeAnalyzer::VISIBILITY_PRIVATE => PhpParser\Modifiers::PRIVATE,
                ClassLikeAnalyzer::VISIBILITY_PROTECTED => PhpParser\Modifiers::PROTECTED,
                default => PhpParser\Modifiers::PUBLIC,
            };

            $docblock = new ParsedDocblock('', []);

            if ($property_storage->type
                && $property_storage->signature_type !== $property_storage->type
            ) {
                $docblock->tags['var'][] = $property_storage->type->toNamespacedString(
                    $namespace_name,
                    [],
                    null,
                    false
                );
            }

            $property_nodes[] = new VirtualProperty(
                $flag | ($property_storage->is_static ? PhpParser\Modifiers::STATIC : 0),
                [
                    new VirtualPropertyItem(
                        $property_name,
                        $property_storage->suggested_type
                            ? StubsGenerator::getExpressionFromType($property_storage->suggested_type)
                            : null
                    )
                ],
                [
                    'comments' => $docblock->tags
                        ? [
                            new PhpParser\Comment\Doc(
                                rtrim($docblock->render('        '))
                            )
                        ]
                        : []
                ],
                $property_storage->signature_type
                    ? StubsGenerator::getParserTypeFromPsalmType($property_storage->signature_type)
                    : null
            );
        }

        return $property_nodes;
    }

    /**
     * @return list<PhpParser\Node\Stmt\ClassMethod>
     */
    private static function getMethodNodes(ClassLikeStorage $storage): array {
        $namespace_name = implode('\\', array_slice(explode('\\', $storage->name), 0, -1));
        $method_nodes = [];

        foreach ($storage->methods as $method_storage) {
            if (!$method_storage->cased_name) {
                throw new UnexpectedValueException('very bad');
            }

            $flag = match ($method_storage->visibility) {
                ReflectionProperty::IS_PRIVATE => PhpParser\Modifiers::PRIVATE,
                ReflectionProperty::IS_PROTECTED => PhpParser\Modifiers::PROTECTED,
                default => PhpParser\Modifiers::PUBLIC,
            };

            $docblock = new ParsedDocblock('', []);

            foreach ($method_storage->template_types ?: [] as $template_name => $map) {
                $type = array_values($map)[0];

                $docblock->tags['template'][] = $template_name . ' as ' . $type->toNamespacedString(
                    $namespace_name,
                    [],
                    null,
                    false
                );
            }

            foreach ($method_storage->params as $param) {
                if ($param->type && $param->type !== $param->signature_type) {
                    $docblock->tags['param'][] = $param->type->toNamespacedString(
                        $namespace_name,
                        [],
                        null,
                        false
                    ) . ' $' . $param->name;
                }
            }

            if ($method_storage->return_type
                && $method_storage->signature_return_type !== $method_storage->return_type
            ) {
                $docblock->tags['return'][] = $method_storage->return_type->toNamespacedString(
                    $namespace_name,
                    [],
                    null,
                    false
                );
            }

            foreach ($method_storage->throws ?: [] as $exception_name => $_) {
                $docblock->tags['throws'][] = Type::getStringFromFQCLN(
                    $exception_name,
                    $namespace_name,
                    [],
                    null,
                    false
                );
            }

            $method_nodes[] = new VirtualClassMethod(
                $method_storage->cased_name,
                [
                    'flags' => $flag
                        | ($method_storage->is_static ? PhpParser\Modifiers::STATIC : 0)
                        | ($method_storage->abstract ? PhpParser\Modifiers::ABSTRACT : 0),
                    'params' => StubsGenerator::getFunctionParamNodes($method_storage),
                    'returnType' => $method_storage->signature_return_type
                        ? StubsGenerator::getParserTypeFromPsalmType($method_storage->signature_return_type)
                        : null,
                    'stmts' =>  $storage->is_interface || $method_storage->abstract ? null : [],
                ],
                [
                    'comments' => $docblock->tags
                        ? [
                            new PhpParser\Comment\Doc(
                                rtrim($docblock->render('        '))
                            )
                        ]
                        : []
                ]
            );
        }

        return $method_nodes;
    }
}
