<?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 Magento\Mray\Index\Traverser;

use Magento\Mray\CodeStructuralElement\Php\Reflection\ClassLikeDeclaration as PhpClassLikeDeclaration;
use Magento\Mray\CodeStructuralElement\Php\Reflection\InCodeDeclaration;
use Magento\Mray\CodeStructuralElement\Php\Reflection\PropertyDeclaration;
use Magento\Mray\CodeStructuralElement\Php\Reflection\StructuralElementDeclaration as PhpDeclaration;
use Magento\Mray\Index\Collection\MagentoApi;
use Magento\Mray\Index\Data\MagentoApiDeclaration;
use Magento\Mray\Package\AbstractTree\Component\MagentoEcosystemAsset;
use Magento\Mray\Package\AbstractTree\Node\Node;
use Magento\Mray\Package\AbstractTree\Node\Package;
use Magento\Mray\Package\AbstractTree\Node\PhpCode;
use Magento\Mray\Index\Scanner\Tuner\MagentoPhpCodeNodeSetter;
use function preg_match;
use function sprintf;
use function strlen;
use function substr;
use function trim;

class MagentoApiDiscovery implements NodeVisitor
{
    /**
     * @var MagentoApi
     */
    private $api;

    /**
     * @param MagentoApi $api
     */
    public function __construct(MagentoApi $api)
    {
        $this->api = $api;
    }

    /**
     * @inheritDoc
     */
    public function enterNode(Node $node)
    {
        if (!$node instanceof Package) {
            return null;
        }
        if (!$node instanceof MagentoEcosystemAsset) {
            return null;
        }
        if (!isset($node->{MagentoPhpCodeNodeSetter::NODE_PHP_CODE})) {
            return null;
        }
        $phpCode = $node->{MagentoPhpCodeNodeSetter::NODE_PHP_CODE};
        if (!$phpCode instanceof PhpCode) {
            return null;
        }
        if (!$phpCode->getDeclarations()) {
            return;
        }
        foreach ($this->getReferencesToIndex($phpCode->getDeclarations()) as $fqsen => $info) {
            if ($info === false || !isset($info['since'])) {
                $this->api->registerNonApi($fqsen);
                continue;
            }
            $this->api->register($fqsen, new MagentoApiDeclaration($info));
        }
    }

    /**
     * @param array $declarations
     * @return iterable
     */
    private function getReferencesToIndex(array $declarations): iterable
    {
        foreach ($declarations as $ds) {
            foreach ($ds as $declaration) {
                if ($declaration instanceof PhpDeclaration) { // todo: make api detectors injectable
                    yield from $this->checkPhpApiDeclarations($declaration);
                }
            }
        }
        return;
        // phpcs:ignore Squiz.PHP.NonExecutableCode
        yield;
    }

    /**
     * @param PhpDeclaration $declaration
     * @return iterable
     */
    private function checkPhpApiDeclarations(PhpDeclaration $declaration): iterable
    {
        if (!$declaration instanceof InCodeDeclaration) {
            return;
            // phpcs:ignore Squiz.PHP.NonExecutableCode
            yield;
        }
        if (!$declaration->getName()) {
            return;
            // phpcs:ignore Squiz.PHP.NonExecutableCode
            yield;
        }

        $api = $this->getApiInformationFromPhpDoc($declaration);
        if (!empty($api)) {
            yield $declaration->getName() => $api;
        } elseif ($declaration instanceof PhpClassLikeDeclaration) {
            yield $declaration->getName() => false;
        }

        if (!$declaration instanceof PhpClassLikeDeclaration) {
            return;
            // phpcs:ignore Squiz.PHP.NonExecutableCode
            yield;
        }

        foreach ($declaration->getMembers() as $memberDeclaration) {
            if (!$memberDeclaration->getName()) {
                continue;
            }

            if ($memberDeclaration->isPrivate()) {
                continue;
            }

            $fqsen = sprintf(
                '%s::%s%s',
                $declaration->getName(),
                $memberDeclaration instanceof PropertyDeclaration ? '$' : '',
                $memberDeclaration->getName()
            );

            // members of parent classes
            if ($memberDeclaration->getClassLikeName() !== $declaration->getName()) {
                // use only protected members of direct parent
                if ($memberDeclaration->isProtected()) {
                    continue;
                }

                if (empty($api)) {
                    yield $fqsen => false;
                    continue;
                }
            }

            $memberApiInfo = [];
            if ($memberDeclaration instanceof InCodeDeclaration) {
                $memberApiInfo = $this->getApiInformationFromPhpDoc($memberDeclaration);
            }
            // phpcs:ignore Magento2.Performance.ForeachArrayMerge
            $memberApiInfo = array_merge($api, $memberApiInfo);

            yield $fqsen => ($memberApiInfo ?: false);
        }

        return;
        // phpcs:ignore Squiz.PHP.NonExecutableCode
        yield;
    }

    /**
     * @param InCodeDeclaration $declaration
     * @return array
     */
    private function getApiInformationFromPhpDoc(InCodeDeclaration $declaration): array
    {
        $phpDoc = $declaration->getPhpDoc();
        if (!$phpDoc) {
            return [];
        }
        $info = [];

        $arrayApiTags = $phpDoc->getTagsByName('@api');
        $arraySinceTags = $phpDoc->getTagsByName('@since');
        $arraySeeTags = $phpDoc->getTagsByName('@see');

        $apiTag = end($arrayApiTags);
        $apiSinceTag = end($arraySinceTags);

        if ($apiTag) {
            $info['since'] = [];
        }

        if ($apiSinceTag) {
            $sinceVersion = $this->extractVersion((string)$apiSinceTag->value);
            if ($sinceVersion) {
                $info['since'] = $sinceVersion;
            }
        }

        $arrayDeprecatedTags = $phpDoc->getDeprecatedTagValues();
        $deprecatedTag = end($arrayDeprecatedTags);
        $seeTag = end($arraySeeTags);

        if ($deprecatedTag) {
            $info['deprecated'] = [];
            $deprecationComment = $deprecatedTag->description;
            $deprecationVersion = $this->extractVersion($deprecationComment);
            if ($deprecationVersion &&
                substr($deprecationComment, 0, strlen($deprecationVersion)) === $deprecationVersion
            ) {
                $deprecationComment = trim(substr($deprecationComment, strlen($deprecationVersion)));
            }

            if ($deprecationVersion) {
                $info['deprecated']['since'] = $deprecationVersion;
            }
            if ($deprecationComment) {
                $info['deprecated']['comment'] = $deprecationComment;
            }
            $info['deprecated']['see'] = $seeTag->value->value ?? null;
        }

        return $info;
    }

    /**
     * @param string $info
     * @return mixed|null
     */
    private function extractVersion(string $info)
    {
        if (preg_match(
            '/(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?P<suffix>-?\S*)/',
            $info,
            $semVer
        )) {
            return $semVer[0];
        }
        return null;
    }

    /**
     * @inheritDoc
     */
    public function leaveNode(Node $node)
    {
        return null;
    }
}
