<?php

declare(strict_types=1);

namespace Psalm\Internal\Analyzer\Statements\Expression\Fetch;

use PhpParser;
use Psalm\Aliases;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\NamespaceAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\SimpleTypeInferer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\UndefinedConstant;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Union;
use ReflectionProperty;

use function array_key_exists;
use function array_pop;
use function explode;
use function implode;
use function strtolower;

/**
 * @internal
 */
final class ConstFetchAnalyzer
{
    public static function analyze(
        StatementsAnalyzer $statements_analyzer,
        PhpParser\Node\Expr\ConstFetch $stmt,
        Context $context,
    ): void {
        $const_name = $stmt->name->toString();

        switch (strtolower($const_name)) {
            case 'null':
                $statements_analyzer->node_data->setType($stmt, Type::getNull());
                break;

            case 'false':
                // false is a subtype of bool
                $statements_analyzer->node_data->setType($stmt, Type::getFalse());
                break;

            case 'true':
                $statements_analyzer->node_data->setType($stmt, Type::getTrue());
                break;

            case 'stdin':
                $statements_analyzer->node_data->setType($stmt, Type::getResource());
                break;

            default:
                if (isset($statements_analyzer->getCodebase()->config->forbidden_constants[$const_name])) {
                    IssueBuffer::maybeAdd(
                        new ForbiddenCode(
                            'You have forbidden the use of ' . $const_name,
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
                        ),
                        $statements_analyzer->getSuppressedIssues(),
                    );
        
                    return;
                }
    
                $const_type = self::getConstType(
                    $statements_analyzer,
                    $const_name,
                    $stmt->name instanceof PhpParser\Node\Name\FullyQualified,
                    $context,
                );

                $codebase = $statements_analyzer->getCodebase();

                $aliased_constants = $statements_analyzer->getAliases()->constants;
                if (isset($aliased_constants[$const_name])) {
                    $fq_const_name = $aliased_constants[$const_name];
                } elseif ($stmt->name instanceof PhpParser\Node\Name\FullyQualified) {
                    $fq_const_name = $const_name;
                } else {
                    $fq_const_name = Type::getFQCLNFromString($const_name, $statements_analyzer->getAliases());
                }

                $codebase->analyzer->addNodeReference(
                    $statements_analyzer->getFilePath(),
                    $stmt,
                    $const_type
                        ? $fq_const_name
                        : '*'
                            . ($stmt->name instanceof PhpParser\Node\Name\FullyQualified
                                ? '\\'
                                : $statements_analyzer->getNamespace() . '-')
                            . $const_name,
                );

                if ($const_type) {
                    $statements_analyzer->node_data->setType($stmt, $const_type);
                } elseif ($context->check_consts) {
                    IssueBuffer::maybeAdd(
                        new UndefinedConstant(
                            'Const ' . $const_name . ' is not defined'.
                                ', consider enabling the allConstantsGlobal config option if scanning legacy codebases',
                            new CodeLocation($statements_analyzer->getSource(), $stmt),
                        ),
                        $statements_analyzer->getSuppressedIssues(),
                    );
                }
        }
    }

    public static function getGlobalConstType(
        Codebase $codebase,
        string $fq_const_name,
        string $const_name,
    ): ?Union {
        if ($const_name === 'STDERR'
            || $const_name === 'STDOUT'
            || $const_name === 'STDIN'
        ) {
            return Type::getResource();
        }

        if ($fq_const_name) {
            $stubbed_const_type = $codebase->getStubbedConstantType(
                $fq_const_name,
            );

            if ($stubbed_const_type) {
                return $stubbed_const_type;
            }
        }

        $stubbed_const_type = $codebase->getStubbedConstantType(
            $const_name,
        );

        if ($stubbed_const_type) {
            return $stubbed_const_type;
        }

        $predefined_constants = $codebase->config->getPredefinedConstants();

        if (($fq_const_name && array_key_exists($fq_const_name, $predefined_constants))
            || array_key_exists($const_name, $predefined_constants)
        ) {
            switch ($const_name) {
                case 'DIRECTORY_SEPARATOR':
                case 'PATH_SEPARATOR':
                case 'PHP_EOL':
                    return Type::getSingleLetter();

                case 'PHP_VERSION':
                    return Type::getNonEmptyString();

                case 'PEAR_EXTENSION_DIR':
                case 'PEAR_INSTALL_DIR':
                case 'PHP_BINARY':
                case 'PHP_BINDIR':
                case 'PHP_CONFIG_FILE_PATH':
                case 'PHP_CONFIG_FILE_SCAN_DIR':
                case 'PHP_DATADIR':
                case 'PHP_EXTENSION_DIR':
                case 'PHP_EXTRA_VERSION':
                case 'PHP_LIBDIR':
                case 'PHP_LOCALSTATEDIR':
                case 'PHP_MANDIR':
                case 'PHP_OS':
                case 'PHP_OS_FAMILY':
                case 'PHP_PREFIX':
                case 'PHP_SAPI':
                case 'PHP_SYSCONFDIR':
                    return Type::getString();

                case 'PHP_MAJOR_VERSION':
                case 'PHP_MINOR_VERSION':
                case 'PHP_RELEASE_VERSION':
                case 'PHP_DEBUG':
                case 'PHP_FLOAT_DIG':
                case 'PHP_INT_MIN':
                case 'PHP_ZTS':
                    return Type::getInt();

                case 'PHP_INT_MAX':
                case 'PHP_INT_SIZE':
                case 'PHP_MAXPATHLEN':
                case 'PHP_VERSION_ID':
                    return Type::getIntRange(1, null);

                case 'PHP_FLOAT_EPSILON':
                case 'PHP_FLOAT_MAX':
                case 'PHP_FLOAT_MIN':
                    return Type::getFloat();
            }

            if ($fq_const_name && array_key_exists($fq_const_name, $predefined_constants)) {
                return ClassLikeAnalyzer::getTypeFromValue($predefined_constants[$fq_const_name]);
            }

            return ClassLikeAnalyzer::getTypeFromValue($predefined_constants[$const_name]);
        }

        return null;
    }

    public static function getConstType(
        StatementsAnalyzer $statements_analyzer,
        string $const_name,
        bool $is_fully_qualified,
        ?Context $context,
    ): ?Union {
        $aliased_constants = $statements_analyzer->getAliases()->constants;

        if (isset($aliased_constants[$const_name])) {
            $fq_const_name = $aliased_constants[$const_name];
        } elseif ($is_fully_qualified) {
            $fq_const_name = $const_name;
        } else {
            $fq_const_name = Type::getFQCLNFromString($const_name, $statements_analyzer->getAliases());
        }

        if ($fq_const_name) {
            $const_name_parts = explode('\\', $fq_const_name);
            $const_name = array_pop($const_name_parts);
            $namespace_name = implode('\\', $const_name_parts);
            $namespace_constants = NamespaceAnalyzer::getConstantsForNamespace(
                $namespace_name,
                ReflectionProperty::IS_PUBLIC,
            );

            if (isset($namespace_constants[$const_name])) {
                return $namespace_constants[$const_name];
            }
        }

        if ($context && $context->hasVariable($fq_const_name)) {
            return $context->vars_in_scope[$fq_const_name];
        }

        $file_path = $statements_analyzer->getRootFilePath();
        $codebase = $statements_analyzer->getCodebase();

        $file_storage_provider = $codebase->file_storage_provider;

        $file_storage = $file_storage_provider->get($file_path);

        if (isset($file_storage->declaring_constants[$const_name])) {
            $constant_file_path = $file_storage->declaring_constants[$const_name];

            return $file_storage_provider->get($constant_file_path)->constants[$const_name];
        }

        if (isset($file_storage->declaring_constants[$fq_const_name])) {
            $constant_file_path = $file_storage->declaring_constants[$fq_const_name];

            return $file_storage_provider->get($constant_file_path)->constants[$fq_const_name];
        }

        return self::getGlobalConstType($codebase, $fq_const_name, $const_name)
            ?? self::getGlobalConstType($codebase, $const_name, $const_name);
    }

    public static function setConstType(
        StatementsAnalyzer $statements_analyzer,
        string $const_name,
        Union $const_type,
        Context $context,
    ): void {
        $context->vars_in_scope[$const_name] = $const_type;
        $context->constants[$const_name] = $const_type;

        $source = $statements_analyzer->getSource();

        if ($source instanceof NamespaceAnalyzer) {
            $source->setConstType($const_name, $const_type);
        }
    }

    public static function getConstName(
        PhpParser\Node\Expr $first_arg_value,
        NodeDataProvider $type_provider,
        Codebase $codebase,
        Aliases $aliases,
    ): ?string {
        $const_name = null;

        if ($first_arg_value instanceof PhpParser\Node\Scalar\String_) {
            $const_name = $first_arg_value->value;
        } elseif ($first_arg_type = $type_provider->getType($first_arg_value)) {
            if ($first_arg_type->isSingleStringLiteral()) {
                $const_name = $first_arg_type->getSingleStringLiteral()->value;
            }
        } else {
            $simple_type = SimpleTypeInferer::infer($codebase, $type_provider, $first_arg_value, $aliases);

            if ($simple_type && $simple_type->isSingleStringLiteral()) {
                $const_name = $simple_type->getSingleStringLiteral()->value;
            }
        }

        return $const_name;
    }

    public static function analyzeConstAssignment(
        StatementsAnalyzer $statements_analyzer,
        PhpParser\Node\Stmt\Const_ $stmt,
        Context $context,
    ): void {
        foreach ($stmt->consts as $const) {
            ExpressionAnalyzer::analyze($statements_analyzer, $const->value, $context);

            self::setConstType(
                $statements_analyzer,
                $const->name->name,
                $statements_analyzer->node_data->getType($const->value) ?? Type::getMixed(),
                $context,
            );
        }
    }
}
