<?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\CodeStructuralElement\Php\NodeVisitor;

use PhpParser\Node;
use PhpParser\NodeVisitor;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use function array_filter;
use function array_merge;
use function array_pop;
use function array_push;
use function count;
use function preg_match;
use function strlen;
use function substr;

class MagentoApiRecognition implements NodeVisitor
{
    /**
     * @var array
     */
    private $stack = [];

    /**
     * @return Node|null
     */
    private function current(): ?Node
    {
        return $this->stack[count($this->stack) - 1] ?? null;
    }

    /**
     * @param Node $node
     */
    private function push(Node $node)
    {
        array_push($this->stack, $node);
    }

    /**
     * Extract the last element of stack array.
     */
    private function pop()
    {
        array_pop($this->stack);
    }

    /**
     * @inheritDoc
     */
    public function beforeTraverse(array $nodes)
    {
        return null;
    }

    /**
     * @inheritDoc
     */
    public function enterNode(Node $node)
    {
        if (!(
            $node instanceof Node\Stmt\ClassLike ||
            $node instanceof Node\Stmt\ClassConst ||
            $node instanceof Node\Stmt\Property ||
            $node instanceof Node\Stmt\ClassMethod ||
            $node instanceof Node\Stmt\Function_ ||
            $node instanceof Node\Stmt\Const_
        )) {
            return null;
        }

        $current = $this->current();
        $magento = $current->magento ?? null;

        /** @var PhpDocNode $phpDoc */
        $phpDoc = $node->getAttribute(PhpDocAst::PHP_DOC_COMMENT);
        if (!$phpDoc) {
            if (isset($magento)) {
                $node->magento = $magento;
            }
            return null;
        }

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

            $apiTag = end($arrayApiTags);
            $apiSinceTag = end($arraySinceTags);
            $deprecatedTag = end($arrayDeprecatedTags);
            $seeTag = end($arraySeeTags);
        }
        if ($apiTag) {
            $magento = $this->setMagentoInfo($magento, 'api', [
                'since' => $apiSinceTag ? $this->extractVersion((string)$apiSinceTag->value) : null,
            ]);
        }
        if ($deprecatedTag) {
            $deprecationComment = $deprecatedTag->description;
            $deprecationVersion = $this->extractVersion($deprecationComment);
            if ($deprecationVersion &&
                substr($deprecationComment, 0, strlen($deprecationVersion)) === $deprecationVersion
            ) {
                $deprecationComment = trim(substr($deprecationComment, strlen($deprecationVersion)));
            }

            $magento = $this->setMagentoInfo($magento, 'deprecated', [
                'since' => $deprecationVersion,
                'comment' => $deprecationComment,
                'see' => $seeTag->value ?? null
            ]);
        }

        if ($magento) {
            $node->magento = $magento;
            $this->push($node);
        }
    }

    /**
     * @param object|null $magento
     * @param string $key
     * @param array $value
     * @return object|null
     */
    private function setMagentoInfo(?object $magento, string $key, array $value)
    {
        $value = array_filter($value);
        if (empty($value) && isset($magento->{$key})) {
            return $magento;
        }

        if (!$magento) {
            $magento = (object)[];
        } else {
            $magento = clone $magento;
        }
        if (isset($magento->{$key})) {
            $value = array_merge((array)$magento->{$key}, $value);
        }
        $magento->{$key} = (object)$value;
        return $magento;
    }

    /**
     * @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)
    {
        if ($this->current() === $node) {
            $this->pop();
        }
        return null;
    }

    /**
     * @inheritDoc
     */
    public function afterTraverse(array $nodes)
    {
        return null;
    }
}
