<?php
namespace Psalm\Internal\Analyzer\Statements\Expression;

use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\MethodAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\MethodIdentifier;
use Psalm\Issue\InvalidClone;
use Psalm\Issue\MixedClone;
use Psalm\Issue\PossiblyInvalidClone;
use Psalm\IssueBuffer;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TTemplateParam;

use function array_merge;
use function array_pop;

class CloneAnalyzer
{
    public static function analyze(
        StatementsAnalyzer $statements_analyzer,
        PhpParser\Node\Expr\Clone_ $stmt,
        Context $context
    ): bool {
        $codebase = $statements_analyzer->getCodebase();
        $codebase_methods = $codebase->methods;
        if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
            return false;
        }

        $stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr);

        if ($stmt_expr_type) {
            $clone_type = $stmt_expr_type;

            $immutable_cloned = false;

            $invalid_clones = [];
            $mixed_clone = false;

            $possibly_valid = false;
            $atomic_types = $clone_type->getAtomicTypes();

            while ($atomic_types) {
                $clone_type_part = array_pop($atomic_types);

                if ($clone_type_part instanceof TMixed) {
                    $mixed_clone = true;
                } elseif ($clone_type_part instanceof TObject) {
                    $possibly_valid = true;
                } elseif ($clone_type_part instanceof TNamedObject) {
                    if (!$codebase->classlikes->classOrInterfaceExists($clone_type_part->value)) {
                        $invalid_clones[] = $clone_type_part->getId();
                    } else {
                        $clone_method_id = new MethodIdentifier(
                            $clone_type_part->value,
                            '__clone'
                        );

                        $does_method_exist = $codebase_methods->methodExists(
                            $clone_method_id,
                            $context->calling_method_id,
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
                        );
                        $is_method_visible = MethodAnalyzer::isMethodVisible(
                            $clone_method_id,
                            $context,
                            $statements_analyzer->getSource()
                        );
                        if ($does_method_exist && !$is_method_visible) {
                            $invalid_clones[] = $clone_type_part->getId();
                        } else {
                            $possibly_valid = true;
                            $immutable_cloned = true;
                        }
                    }
                } elseif ($clone_type_part instanceof TTemplateParam) {
                    $atomic_types = array_merge($atomic_types, $clone_type_part->as->getAtomicTypes());
                } else {
                    if ($clone_type_part instanceof TFalse
                        && $clone_type->ignore_falsable_issues
                    ) {
                        continue;
                    }

                    if ($clone_type_part instanceof TNull
                        && $clone_type->ignore_nullable_issues
                    ) {
                        continue;
                    }

                    $invalid_clones[] = $clone_type_part->getId();
                }
            }

            if ($mixed_clone) {
                IssueBuffer::maybeAdd(
                    new MixedClone(
                        'Cannot clone mixed',
                        new CodeLocation($statements_analyzer->getSource(), $stmt)
                    ),
                    $statements_analyzer->getSuppressedIssues()
                );
            }

            if ($invalid_clones) {
                if ($possibly_valid) {
                    IssueBuffer::maybeAdd(
                        new PossiblyInvalidClone(
                            'Cannot clone ' . $invalid_clones[0],
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
                        ),
                        $statements_analyzer->getSuppressedIssues()
                    );
                } else {
                    IssueBuffer::maybeAdd(
                        new InvalidClone(
                            'Cannot clone ' . $invalid_clones[0],
                            new CodeLocation($statements_analyzer->getSource(), $stmt)
                        ),
                        $statements_analyzer->getSuppressedIssues()
                    );
                }

                return true;
            }

            $statements_analyzer->node_data->setType($stmt, $stmt_expr_type);

            if ($immutable_cloned) {
                $stmt_expr_type = clone $stmt_expr_type;
                $statements_analyzer->node_data->setType($stmt, $stmt_expr_type);
                $stmt_expr_type->reference_free = true;
                $stmt_expr_type->allow_mutations = true;
            }
        }

        return true;
    }
}
