<?php
/**
 * Copyright 2020 Adobe
 * All Rights Reserved.
 *
 * NOTICE: Adobe permits you to use, modify, and distribute this file in
 * accordance with the terms of the Adobe license agreement accompanying
 * it.
 */
declare(strict_types=1);

namespace Sut\Domain\Compatibility\Analyzers\Php;

use Sut\Domain\Compatibility\Analyzers\AnalyzerInterface;
use Sut\Domain\Compatibility\Analyzers\Php\MethodAnalyzer\MatchTypes;
use Sut\Domain\Compatibility\GetCodeGenerationBase;
use Sut\Domain\Compatibility\Index;
use Sut\Domain\Issue\DTO\Issue;
use Sut\Domain\Issue\IssueFactory;
use Sut\Domain\MRay\DTO\MethodSignature;
use Sut\Domain\MRay\DTO\Module\Dependency;
use Sut\Domain\MRay\DTO\Module\DependencyUsage;
use Sut\Domain\MRay\DTO\Module\Parameter;

final class MethodAnalyzer implements AnalyzerInterface
{
    private const USAGES = [
        'call',
        'extend'
    ];

    private const INHERITED_CODES = [
        'call' => 1428
    ];

    private const NON_API_CODES = [
        'call' => 1429,
        'instantiation' => 1420
    ];

    private const NON_EXISTENT_CODES = [
        'call' => 1410,
        'instantiation' => 1110
    ];

    private const DEPRECATED_CODES = [
        'call' => 1439
    ];

    private const CODE_NON_DECLARED_MAGIC_METHOD = 1430;

    private const ISSUE_TYPE_METHOD_SIGNATURE_MISMATCH = 'MethodSignatureMismatch';

    private const DATA_OBJECT_MAGIC_METHODS_PREFIXES = [
        'set',
        'get',
        'has',
        'uns'
    ];

    /**
     * @var Index
     */
    private $index;

    /**
     * @var IssueFactory
     */
    private $issueFactory;

    /**
     * @var GetCodeGenerationBase
     */
    private $getCodeGenerationBase;

    /**
     * @var MatchTypes
     */
    private $matchTypes;

    /**
     * @param Index $index
     * @param IssueFactory $issueFactory
     * @param GetCodeGenerationBase $getCodeGenerationBase
     * @param MatchTypes $matchTypes
     */
    public function __construct(
        Index $index,
        IssueFactory $issueFactory,
        GetCodeGenerationBase $getCodeGenerationBase,
        MatchTypes $matchTypes
    ) {
        $this->index = $index;
        $this->issueFactory = $issueFactory;
        $this->getCodeGenerationBase = $getCodeGenerationBase;
        $this->matchTypes = $matchTypes;
    }

    /**
     * @inheritdoc
     */
    public function analyze(Dependency $dependency, DependencyUsage $usage, string $version): array
    {
        if ($dependency->getType() !== 'method') {
            return [];
        }

        if (!in_array($usage->getType(), self::USAGES)) {
            return [];
        }

        $fqn = $dependency->getFqn();

        $classMethod = explode('::', $fqn);

        if (count($classMethod) !== 2) {
            return [];
        }

        $methodName = end($classMethod);

        if ($methodName === '__construct') {
            return [];
        }

        $issues = [];

        $methodSignature = $this->index->getMethodSignature($fqn, $version);

        if ($usage->getType() === 'extend') {
            if ($methodSignature) {
                return $this->getMethodSignatureIssues($usage, $fqn, $methodSignature);
            }
            return [];
        }

        if ($usage->getType() === 'call' && $methodSignature) {
            $issues = $this->getArgumentsIssues($usage, $fqn, $methodSignature);
        }

        if ($this->index->isApi($fqn, $version)) {
            if ($this->index->isDeprecated($fqn, $version)) {
                $issues[] = $this->issueFactory->createIssue(
                    self::DEPRECATED_CODES[$usage->getType()],
                    [$fqn, $version, $this->index->getDeprecatedRecommendation($fqn, $version)],
                    $usage->getPosition(),
                    $usage->getFile()
                );
            }

            return $issues;
        }

        if (!$this->index->isPresent($fqn, $version) && $dependency->getOriginalFactory()) {
            return $this->getFactoryCreateCallIssues($dependency, $usage, $version);
        }

        if ($this->index->isApi($dependency->getRealName(), $version)) {
            $issues[] = $this->issueFactory->createIssue(
                self::INHERITED_CODES[$usage->getType()],
                [$dependency->getRealName(), $fqn],
                $usage->getPosition(),
                $usage->getFile()
            );

            return $issues;
        }

        if (!$this->index->isPresent($fqn, $version)) {
            if ($this->isDataObjectMagicMethod($methodName)) {
                $issues[] = $this->issueFactory->createIssue(
                    self::CODE_NON_DECLARED_MAGIC_METHOD,
                    [$fqn, $version],
                    $usage->getPosition(),
                    $usage->getFile()
                );
                return $issues;
            }
            $issues[] = $this->issueFactory->createIssue(
                self::NON_EXISTENT_CODES[$usage->getType()],
                [$fqn, $version],
                $usage->getPosition(),
                $usage->getFile()
            );
            return $issues;
        }

        if (!$this->index->isApi($fqn, $version)) {
            $issues[] = $this->issueFactory->createIssue(
                self::NON_API_CODES[$usage->getType()],
                [$fqn, $version],
                $usage->getPosition(),
                $usage->getFile()
            );
        }

        return $issues;
    }

    /**
     * @param Dependency $dependency
     * @param DependencyUsage $usage
     * @param string $version
     * @return array
     */
    private function getFactoryCreateCallIssues(
        Dependency $dependency,
        DependencyUsage $usage,
        string $version
    ): array {
        $issues = [];

        $fqn = $dependency->getFqn();
        $fqnParts = explode('::', $fqn);
        $baseFqn = $this->getCodeGenerationBase->execute($fqnParts[0]);

        if (!$this->index->isPresent($baseFqn, $version)) {
            $issues[] = $this->issueFactory->createIssue(
                self::NON_EXISTENT_CODES['instantiation'],
                [$dependency->getOriginalFactory(), $version],
                $usage->getPosition(),
                $usage->getFile()
            );
            return $issues;
        }

        if (!$this->index->isApi($baseFqn, $version)) {
            $issues[] = $this->issueFactory->createIssue(
                self::NON_API_CODES['instantiation'],
                [$dependency->getOriginalFactory(), $version],
                $usage->getPosition(),
                $usage->getFile()
            );
        }

        return $issues;
    }

    /**
     * @param string $methodName
     * @return bool
     */
    private function isDataObjectMagicMethod(string $methodName): bool
    {
        foreach (self::DATA_OBJECT_MAGIC_METHODS_PREFIXES as $prefix) {
            if (strpos($methodName, $prefix) === 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param DependencyUsage $usage
     * @param string $fqn
     * @param MethodSignature $apiSignature
     * @return Issue[]
     */
    private function getMethodSignatureIssues(DependencyUsage $usage, string $fqn, MethodSignature $apiSignature): array
    {
        $issues = [];
        $expectedReturnType = $apiSignature->getReturnType();
        $actualReturnType = $usage->getMethodSignature()->getReturnType();
        if (!$this->matchTypes->execute($actualReturnType, $expectedReturnType)) {
            $issues[] = $this->issueFactory->createIssueByType(
                self::ISSUE_TYPE_METHOD_SIGNATURE_MISMATCH,
                sprintf(
                    'Overridden core method "%s" signature mismatch: Expected return type: "%s", actual: "%s"',
                    $fqn,
                    $expectedReturnType,
                    $actualReturnType
                ),
                $usage->getPosition(),
                $usage->getFile()
            );
        }
        $messages = $this->getParametersMismatchIssues($usage->getMethodSignature(), $apiSignature);

        foreach ($messages as $message) {
            $issues[] = $this->issueFactory->createIssueByType(
                self::ISSUE_TYPE_METHOD_SIGNATURE_MISMATCH,
                sprintf('Overridden core method "%s" signature mismatch: %s', $fqn, $message),
                $usage->getPosition(),
                $usage->getFile()
            );
        }
        return $issues;
    }

    /**
     * @param DependencyUsage $usage
     * @param string $fqn
     * @param MethodSignature $apiSignature
     * @return Issue[]
     */
    private function getArgumentsIssues(DependencyUsage $usage, string $fqn, MethodSignature $apiSignature): array
    {
        $parameters = $apiSignature->getParameters();
        if (empty($parameters)) {
            return [];
        }

        $arguments = $usage->getMethodSignature()->getParameters();
        $issues = [];
        foreach ($parameters as $index => $parameter) {
            $issues[] = $this->getArgumentIssue($fqn, $index, $usage, $parameter, $arguments);
        }

        return array_filter($issues);
    }

    /**
     * @param string $fqn
     * @param int $index
     * @param DependencyUsage $usage
     * @param Parameter $parameter
     * @param array $arguments
     * @return Issue|null
     */
    private function getArgumentIssue(
        string $fqn,
        int $index,
        DependencyUsage $usage,
        Parameter $parameter,
        array $arguments
    ): ?Issue {
        if (!isset($arguments[$index])) {
            if ($parameter->isOptional()) {
                return null;
            }
            return $this->issueFactory->createIssueByType(
                self::ISSUE_TYPE_METHOD_SIGNATURE_MISMATCH,
                sprintf(
                    'Method "%s" expects required parameter %s ("%s") of type "%s"',
                    $fqn,
                    $index + 1,
                    $parameter->getName(),
                    $parameter->getType()
                ),
                $usage->getPosition(),
                $usage->getFile()
            );
        }

        if (!$this->matchTypes->execute($parameter->getType(), $arguments[$index]->getType())) {
            return $this->issueFactory->createIssueByType(
                self::ISSUE_TYPE_METHOD_SIGNATURE_MISMATCH,
                sprintf(
                    'Method "%s" expects parameter %s ("%s") of type "%s", received "%s" instead',
                    $fqn,
                    $index + 1,
                    $parameter->getName(),
                    $parameter->getType(),
                    $arguments[$index]->getType()
                ),
                $usage->getPosition(),
                $usage->getFile()
            );
        }
        return null;
    }

    /**
     * @param MethodSignature $actualSignature
     * @param MethodSignature $expectedSignature
     * @return string[]
     */
    private function getParametersMismatchIssues(
        MethodSignature $actualSignature,
        MethodSignature $expectedSignature
    ): array {
        $actualParameters = $actualSignature->getParameters();
        $expectedParameters = $expectedSignature->getParameters();

        $messages = [];

        foreach ($expectedParameters as $index => $expectedParameter) {
            if (!isset($actualParameters[$index])) {
                $messages[] = sprintf(
                    'Expected parameter "%s" type "%s" is missing',
                    $expectedParameter->getName(),
                    $expectedParameter->getType()
                );
                continue;
            }
            if ($expectedParameter->isOptional() !== $actualParameters[$index]->isOptional()) {
                $messages[] = sprintf(
                    'Parameter %s should %sbe optional',
                    $expectedParameter->getName(),
                    $expectedParameter->isOptional() ? '' : 'not '
                );
            }

            if (!$this->matchTypes->execute($expectedParameter->getType(), $actualParameters[$index]->getType())) {
                $messages[] = sprintf(
                    'Parameter type "%s" does not match expected "%s"',
                    $actualParameters[$index]->getType(),
                    $expectedParameter->getType()
                );
            }
        }
        return $messages;
    }
}
