<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;

use PhpParser\Node;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Find usages of defined methods within a class that are deprecated/removed.
 * Requires to extend a TYPO3 API class/abstract.
 * This is a strong match.
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
 */
class AbstractMethodImplementationMatcher extends AbstractCoreMatcher
{
    protected const DEFINITION_STATIC = 'static';
    protected const DEFINITION_LOCAL  = 'local';
    protected array $matcherDefinitionLookup = [];

    /**
     * @param array $matcherDefinitions Incoming main configuration
     */
    public function __construct(array $matcherDefinitions)
    {
        $this->matcherDefinitions = $matcherDefinitions;
        $this->validateMatcherDefinitions();
        $this->initializeFlatMatcherDefinitions();

        //  initializeFlatMatcherDefinitions() unfortunately does not deliver the actual
        //  method, so we need to do something 99% similar here for a custom
        //  property, to not require larger changes to the underlying abstract method.
        foreach ($this->matcherDefinitions as $classAndMethod => $details) {
            $parts = GeneralUtility::trimExplode('::', $classAndMethod);
            $definition = self::DEFINITION_STATIC;
            if (count($parts) !== 2) {
                $parts = GeneralUtility::trimExplode('->', $classAndMethod);
                $definition = self::DEFINITION_LOCAL;
            }
            // Exception-Handling removed, covered by initializeFlatMatcherDefinitions();

            $method = $parts[1];
            $class  = $parts[0];
            if (!array_key_exists($class, $this->matcherDefinitionLookup)) {
                $this->matcherDefinitionLookup[$class][$definition][$method]['candidates'] = [];
            }
            $this->matcherDefinitionLookup[$class][$definition][$method]['candidates'][] = $details;

            // Builds something like:
            // [
            //    'TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper' => [
            //        'static' => [
            //            'renderStatic' => [
            //                'candidates' => [
            //                    [
            //                        'restFiles' => [
            //                            'Deprecation-104789-RenderStaticForFluidViewHelpers.rst',
            //                        ],
            //                    ],
            //                ],
            //            ]
            //        ],
            //    ],
            //    'TYPO3\CMS\AbstractSomething' => [
            //        'local' => [
            //            'someMethodName' => [
            //                'candidates' => [
            //                    [
            //                        'restFiles' => [
            //                            'Breaking-12345-something.rst',
            //                        ],
            //                    ],
            //                    [
            //                        'restFiles' => [
            //                            'Breaking-67890-something.rst',
            //                        ],
            //                    ],
            //                ],
            //            ]
            //        ],
            //    ],
            // ];
        }
    }

    /**
     * Called by PhpParser.
     * Test for a defined method that shall longer be utilized (strong match)
     */
    public function enterNode(Node $node): null
    {
        if (!$this->isFileIgnored($node)
            && !$this->isLineIgnored($node)
            && $node instanceof Node\Stmt\Class_
            && $node->extends) {

            // We found a class definition.
            // Check what classes this definition is extending (Abstract).
            // Without a class extending something, this is not API usage and thus not scanned.
            // Now check if the extended class is part of our matcherDefinition to inspect
            if (array_key_exists($node->extends->name, $this->matcherDefinitionLookup)) {

                // Iterate all declared methods (of the inspected custom class, NOT the abstract!)
                $lookupMethods = $this->matcherDefinitionLookup[$node->extends->name];
                foreach ($node->getMethods() as $method) {

                    // The matcherDefinition can utilize 'Abstract::staticMethod' or 'Abstract->localMethod',
                    // which is handled distinctly, so that the matches are stronger.
                    $lookupKey = $method->isStatic() ? self::DEFINITION_STATIC : self::DEFINITION_LOCAL;

                    if (isset($lookupMethods[$lookupKey][$method->name->toString()]['candidates'])) {
                        // The checked method of an object extending a deprecated/BC class was a match.
                        // Gather final match info (multiple ReST files can apply to a single class+method)
                        foreach ($lookupMethods[$lookupKey][$method->name->toString()]['candidates'] as $candidate) {
                            $this->matches[] = [
                                'restFiles' => $candidate['restFiles'],
                                'line' => $method->getAttribute('startLine'),
                                'message' => sprintf(
                                    'Definition of %s method "%s" extends from "%s"',
                                    $lookupKey,
                                    $method->name->toString(),
                                    $node->extends->name
                                ),
                                'indicator' => 'strong',
                            ];
                        }
                    }
                }
            }
        }

        return null;
    }
}
