<?php

namespace Dedoc\Scramble\Infer\Definition;

use Dedoc\Scramble\Infer\Flow\Nodes;
use Dedoc\Scramble\Infer\FlowBuilder;
use Dedoc\Scramble\Infer\Scope\Scope;
use Dedoc\Scramble\Support\Type\Type;
use Dedoc\Scramble\Support\Type\UnknownType;
use PhpParser\Node\FunctionLike;
use PhpParser\NodeTraverser;

class FunctionLikeAstDefinition extends FunctionLikeDefinition
{
    private ?FunctionLikeDefinition $declarationDefinition = null;

    private ?Scope $scope = null;

    private ?FunctionLike $astNode = null;

    private ?Nodes $flowContainer = null;

    public function setDeclarationDefinition(?FunctionLikeDefinition $declarationDefinition): self
    {
        $this->declarationDefinition = $declarationDefinition;

        return $this;
    }

    public function getDeclarationDefinition(): ?FunctionLikeDefinition
    {
        return $this->declarationDefinition;
    }

    public function setAstNode(FunctionLike $astNode): self
    {
        $this->astNode = $astNode;

        return $this;
    }

    public function getAstNode(): FunctionLike
    {
        if (! $this->astNode) {
            /**
             * @see \Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeAstDefinitionBuilder
             */
            throw new \LogicException('AST node must be set before accessing it on FunctionLikeAstDefinition');
        }

        return $this->astNode;
    }

    public function setScope(Scope $scope): self
    {
        $this->scope = $scope;

        return $this;
    }

    public function getScope(): Scope
    {
        if (! $this->scope) {
            /**
             * @see \Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeAstDefinitionBuilder
             */
            throw new \LogicException('Scope must be set before accessing it on FunctionLikeAstDefinition');
        }

        return $this->scope;
    }

    public function getFlowContainer(): Nodes
    {
        if ($this->flowContainer) {
            return $this->flowContainer;
        }

        $traverser = new NodeTraverser;

        $traverser->addVisitor($flowBuilder = new FlowBuilder(
            $this->type->arguments, // parameters
            $this->getScope(),
        ));

        $traverser->traverse([$this->getAstNode()]);

        return $this->flowContainer = $flowBuilder->flowNodes;
    }

    public function getInferredReturnType(): Type
    {
        return $this->type->getReturnType();
    }

    public function getReturnType(): Type
    {
        $inferredReturnType = $this->type->getReturnType();

        if ($inferredReturnType->getAttribute('fromScrambleReturn') === true) {
            return $inferredReturnType;
        }

        if (! $returnDeclarationType = $this->getDeclarationDefinition()?->getReturnType()) {
            return $inferredReturnType;
        }

        return $this->prefersInferredReturnType($returnDeclarationType, $inferredReturnType)
            ? $inferredReturnType
            : $returnDeclarationType;
    }

    private function prefersInferredReturnType(?Type $declarationType, Type $inferredType): bool
    {
        if (! $declarationType || $declarationType instanceof UnknownType) {
            return true;
        }

        if ($declarationType->accepts($inferredType)) {
            return true;
        }

        if ($inferredType->acceptedBy($declarationType)) {
            return true;
        }

        return false;
    }
}
