<?php declare(strict_types=1);

namespace Phan\Language\Element;

use AssertionError;
use Closure;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\FileRef;
use Phan\Language\FQSEN;
use Phan\Language\FQSEN\FullyQualifiedGlobalStructuralElement;
use Phan\Language\UnionType;
use Phan\Memoize;

/**
 * An addressable element is a TypedElement with an FQSEN.
 * (E.g. a class, property, function, method, etc.)
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
 */
abstract class AddressableElement extends TypedElement implements AddressableElementInterface
{
    use Memoize;

    /**
     * @var FQSEN
     * A fully qualified name for the element
     */
    protected $fqsen;

    /**
     * @var array<string,FileRef>
     * A list of locations in which this typed structural
     * element is referenced from.
     * References from the same file and line are deduplicated to save memory.
     */
    protected $reference_list = [];

    /**
     * @var ?string
     * The doc comment of the element
     */
    protected $doc_comment;

    /**
     * @var bool
     * Has this element been hydrated yet?
     * (adding information from ancestor classes for more detailed type information)
     */
    protected $is_hydrated = false;

    /**
     * @param Context $context
     * The context in which the structural element lives
     *
     * @param string $name
     * The name of the typed structural element
     *
     * @param UnionType $type
     * A '|' delimited set of types satisfied by this
     * typed structural element.
     *
     * @param int $flags
     * The flags property contains node specific flags. It is
     * always defined, but for most nodes it is always zero.
     * ast\kind_uses_flags() can be used to determine whether
     * a certain kind has a meaningful flags value.
     *
     * @param FQSEN $fqsen
     * A fully qualified name for the element
     */
    public function __construct(
        Context $context,
        string $name,
        UnionType $type,
        int $flags,
        FQSEN $fqsen
    ) {
        parent::__construct(
            $context,
            $name,
            $type,
            $flags
        );

        $this->setFQSEN($fqsen);
    }

    /**
     * @return FQSEN
     * The fully-qualified structural element name of this
     * structural element
     */
    public function getFQSEN()
    {
        return $this->fqsen;
    }

    /**
     * @param FQSEN $fqsen
     * A fully qualified structural element name to set on
     * this element
     *
     * @return void
     */
    public function setFQSEN(FQSEN $fqsen)
    {
        $this->fqsen = $fqsen;
    }

    /**
     * @return bool true if this element's visibility
     *                   is strictly more visible than $other (public > protected > private)
     */
    public function isStrictlyMoreVisibleThan(AddressableElementInterface $other) : bool
    {
        if ($this->isPrivate()) {
            return false;
        } // $this is public or protected

        if ($other->isPrivate()) {
            return true;
        }

        if ($other->isProtected()) {
            // True if this is public.
            return !$this->isProtected();
        }
        // $other is public
        return false;
    }

    /**
     * @return bool
     * True if this is a public property
     */
    public function isPublic() : bool
    {
        return !(
            $this->isProtected() || $this->isPrivate()
        );
    }

    /**
     * @return bool
     * True if this is a protected element
     */
    public function isProtected() : bool
    {
        return $this->getFlagsHasState(\ast\flags\MODIFIER_PROTECTED);
    }

    /**
     * @return bool
     * True if this is a private element
     */
    public function isPrivate() : bool
    {
        return $this->getFlagsHasState(\ast\flags\MODIFIER_PRIVATE);
    }

    /**
     * @param CodeBase $code_base (@phan-unused-param, this is used by subclasses)
     * The code base in which this element exists.
     *
     * @return bool
     * True if this is marked as an `(at)internal` element
     */
    public function isNSInternal(CodeBase $code_base) : bool
    {
        return $this->getPhanFlagsHasState(Flags::IS_NS_INTERNAL);
    }

    /**
     * Set this element as being `internal`.
     * @return void
     */
    public function setIsNSInternal(bool $is_internal)
    {
        $this->setPhanFlags(Flags::bitVectorWithState(
            $this->getPhanFlags(),
            Flags::IS_NS_INTERNAL,
            $is_internal
        ));
    }

    /**
     * @param CodeBase $code_base (@phan-unused-param, this is used by subclasses)
     * The code base in which this element exists.
     *
     * @return bool
     * True if this element is intern
     */
    public function isNSInternalAccessFromContext(
        CodeBase $code_base,
        Context $context
    ) : bool {
        // Figure out which namespace this element is within
        $element_namespace = $this->getElementNamespace() ?: '\\';

        // Get our current namespace from the context
        $context_namespace = $context->getNamespace() ?: '\\';

        // Test to see if the context is within the same
        // namespace as where the element is defined
        return (0 === \strcasecmp($context_namespace, $element_namespace));
    }

    /**
     * @param FileRef $file_ref
     * A reference to a location in which this typed structural
     * element is referenced.
     *
     * @return void
     */
    public function addReference(FileRef $file_ref)
    {
        if (Config::get_track_references()) {
            // Currently, we don't need to track references to PHP-internal methods/functions/constants
            // such as PHP_VERSION, strlen(), Closure::bind(), etc.
            // This may change in the future.
            if ($this->isPHPInternal()) {
                return;
            }
            $this->reference_list[$file_ref->__toString()] = $file_ref;
        }
    }

    /**
     * Copy addressable references from an element of the same subclass
     * @return void
     */
    public function copyReferencesFrom(AddressableElement $element)
    {
        if ($this === $element) {
            // Should be impossible
            return;
        }
        foreach ($element->reference_list as $key => $file_ref) {
            $this->reference_list[$key] = $file_ref;
        }
    }

    /**
     * @return array<string,FileRef>
     * A list of references to this typed structural element.
     */
    public function getReferenceList() : array
    {
        return $this->reference_list;
    }

    /**
     * @param CodeBase $code_base (@phan-unused-param)
     * Some elements may need access to the code base to
     * figure out their total reference count.
     *
     * @return int
     * The number of references to this typed structural element
     */
    public function getReferenceCount(
        CodeBase $code_base
    ) : int {
        return \count($this->reference_list);
    }

    /**
     * This method must be called before analysis
     * begins.
     *
     * @return void
     * @override
     */
    public function hydrate(CodeBase $code_base)
    {
        if ($this->is_hydrated) {  // Same as isFirstExecution(), inlined due to being called frequently.
            return;
        }
        $this->is_hydrated = true;

        $this->hydrateOnce($code_base);
    }

    /**
     * @return void
     */
    protected function hydrateOnce(CodeBase $unused_code_base)
    {
        // Do nothing unless overridden
    }

    /**
     * Returns the namespace in which this element was declared
     */
    public function getElementNamespace() : string
    {
        $element_fqsen = $this->getFQSEN();
        if (!$element_fqsen instanceof FullyQualifiedGlobalStructuralElement) {
            throw new AssertionError('Expected $this->element_fqsen to be FullyQualifiedGlobalStructuralElement');
        }

        // Figure out which namespace this element is within
        return $element_fqsen->getNamespace();
    }

    /**
     * Used by daemon mode to restore an element to the state it had before parsing.
     * @internal
     * @return ?Closure
     */
    abstract public function createRestoreCallback();

    /**
     * @param ?string $doc_comment the 'docComment' for this element, if any exists.
     * @return void
     */
    public function setDocComment(string $doc_comment = null)
    {
        $this->doc_comment = $doc_comment;
    }

    /**
     * @return ?string the 'docComment' for this element, if any exists.
     */
    public function getDocComment()
    {
        return $this->doc_comment;
    }

    /**
     * @return string the representation of this FQSEN for issue messages.
     * Overridden in some subclasses
     * @suppress PhanUnreferencedPublicMethod (inference error?)
     */
    public function getRepresentationForIssue() : string
    {
        return $this->getFQSEN()->__toString();
    }
}
