<?php

declare(strict_types=1);

namespace Phan\Analysis;

use Phan\CodeBase;
use Phan\Exception\IssueException;
use Phan\Issue;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Flags;
use Phan\Language\UnionType;

/**
 * This analyzer checks if the signatures of inherited properties match
 * and for type mismatches for php 7.4 typed properties.
 */
class CompositionAnalyzer
{

    /**
     * Check to see if the signatures of inherited properties match
     */
    public static function analyzeComposition(
        CodeBase $code_base,
        Clazz $class
    ): void {
        // Get the list of all inherited classes.
        $inherited_class_list =
            $class->getAncestorClassList($code_base);

        // No chance of failed composition if we don't inherit from anything.
        if (!$inherited_class_list) {
            return;
        }

        // Since we're not necessarily getting this list of classes
        // via getClass, we need to ensure that hydration has occurred.
        $class->hydrate($code_base);

        // For each property, find out every inherited class that defines it
        // and check to see if the types line up.
        // (This must be done after hydration, because some properties are loaded from traits)
        foreach ($class->getPropertyMap($code_base) as $property) {
            if ($property->getPhanFlagsHasState(Flags::IS_ENUM_PROPERTY)) {
                continue;
            }
            try {
                $property_union_type = $property->getDefaultType() ?? UnionType::empty();
            } catch (IssueException $_) {
                $property_union_type = UnionType::empty();
            }

            // Check for that property on each inherited
            // class/trait/interface
            foreach ($inherited_class_list as $inherited_class) {
                $inherited_class->hydrate($code_base);

                // Skip any classes/traits/interfaces not defining that
                // property
                if (!$inherited_class->hasPropertyWithName($code_base, $property->getName())) {
                    continue;
                }

                // We don't call `getProperty` because that will create
                // them in some circumstances.
                $inherited_property_map =
                    $inherited_class->getPropertyMap($code_base);

                if (!isset($inherited_property_map[$property->getName()])) {
                    continue;
                }

                // Get the inherited property
                $inherited_property =
                    $inherited_property_map[$property->getName()];

                if ($inherited_property->isDynamicOrFromPHPDoc()) {
                    continue;
                }
                if ($inherited_property->getRealDefiningFQSEN() === $property->getRealDefiningFQSEN()) {
                    continue;
                }

                // Figure out if this property type can cast to the
                // inherited definition's type.
                // Use the phpdoc comment or real type declaration instead of the inferred
                // types from the default to perform this check.
                try {
                    $inherited_property_union_type = $inherited_property->getDefaultType() ?? UnionType::empty();
                } catch (IssueException $_) {
                    $inherited_property_union_type = UnionType::empty();
                }
                // Don't complain about incompatible types if the base property is private, #4426
                if (!$property->isDynamicOrFromPHPDoc() && !$inherited_property->isPrivate()) {
                    $real_property_type = $property->getRealUnionType()->asNormalizedTypes();
                    $real_inherited_property_type = $inherited_property->getRealUnionType()->asNormalizedTypes();
                    if (!$real_property_type->isEqualTo($real_inherited_property_type)) {
                        Issue::maybeEmit(
                            $code_base,
                            $property->getContext(),
                            Issue::IncompatibleRealPropertyType,
                            $property->getFileRef()->getLineNumberStart(),
                            $property->getFQSEN(),
                            $real_property_type,
                            $inherited_property->getFQSEN(),
                            $real_inherited_property_type,
                            $inherited_property->getFileRef()->getFile(),
                            $inherited_property->getFileRef()->getLineNumberStart()
                        );
                    }
                }

                if ($property->getFQSEN() === $property->getRealDefiningFQSEN()) {
                    // No need to warn about incompatible composition of trait with another ancestor if the property's default was overridden
                    continue;
                }
                // TODO: Why expand?
                $can_cast =
                    $property_union_type->canCastToUnionType(
                        $inherited_property_union_type->asExpandedTypes($code_base),
                        $code_base
                    );

                if ($can_cast) {
                    continue;
                }

                // Don't emit an issue if the property suppresses the issue
                // NOTE: The current context is the class, not either of the properties.
                if ($property->checkHasSuppressIssueAndIncrementCount(Issue::IncompatibleCompositionProp)) {
                    continue;
                }

                Issue::maybeEmit(
                    $code_base,
                    $property->getContext(),
                    Issue::IncompatibleCompositionProp,
                    $property->getFileRef()->getLineNumberStart(),
                    (string)$class->getFQSEN(),
                    (string)$inherited_class->getFQSEN(),
                    $property->getName(),
                    (string)$class->getFQSEN(),
                    $property_union_type,
                    $inherited_property_union_type,
                    $class->getFileRef()->getFile(),
                    $class->getFileRef()->getLineNumberStart()
                );
            }
        }
    }
}
