<?php

declare(strict_types=1);

namespace Phan\Plugin\Internal;

use ast;
use ast\Node;
use Phan\Language\Context;
use Phan\Language\Element\Variable;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;

use function array_intersect_key;
use function count;
use function in_array;
use function is_string;

/**
 * This visitor implements the checks for reuse of loop variables.
 */
class LoopVariableReuseVisitor extends PluginAwarePostAnalysisVisitor
{
    // A plugin's visitors should not override visit() unless they need to.

    /**
     * @var list<Node> set by plugin framework
     * @suppress PhanReadOnlyProtectedProperty
     */
    protected $parent_node_list;

    /**
     * @override checks for reuse of variables in a node of kind ast\AST_FOREACH
     */
    public function visitForeach(Node $node): Context
    {
        $this->findVariableReuse($this->extractLoopVariablesOfForeach($node));
        return $this->context;
    }

    /**
     * Extract "loop variables" of foreach loops.
     *
     * @return array<string|int,Node>
     */
    public function extractLoopVariablesOfForeach(Node $node): array
    {
        return $this->extractVariables($node->children['key']) + $this->extractVariables($node->children['value']);
    }

    /**
     * @override checks for reuse of variables in a node of kind ast\AST_FOR
     */
    public function visitFor(Node $node): Context
    {
        $this->findVariableReuse($this->extractLoopVariablesOfFor($node));
        return $this->context;
    }

    /**
     * Extract "loop variables" of for loops.
     *
     * @param Node $node a node of kind ast\AST_FOR
     * @return array<string|int,Node>
     */
    public function extractLoopVariablesOfFor(Node $node): array
    {
        $directions = RedundantConditionLoopCheck::extractComparisonDirections($node->children['cond']) +
            RedundantConditionLoopCheck::extractIncrementDirections($this->code_base, $this->context, $node->children['loop']);
        if (!$directions) {
            return [];
        }
        $variables = self::extractVariables($node->children['cond']) + self::extractVariables($node->children['loop']);
        return array_intersect_key($variables, $directions);
    }

    /**
     * @override checks for reuse of variables in a node of kind ast\AST_WHILE
     */
    public function visitWhile(Node $node): Context
    {
        $this->findVariableReuse($this->extractLoopVariablesOfWhile($node));
        return $this->context;
    }

    /**
     * Extract "loop variables" of while loops.
     *
     * @param Node $node a node of kind ast\AST_WHILE
     * @return array<string|int,Node>
     */
    public function extractLoopVariablesOfWhile(Node $node): array
    {
        $directions = RedundantConditionLoopCheck::extractComparisonDirections($node->children['cond']);
        if (!$directions) {
            return [];
        }
        return array_intersect_key(self::extractVariables($node->children['cond']), $directions);
    }

    /**
     * @param array<string|int,Node> $variables
     */
    private function findVariableReuse(array $variables): void
    {
        if (!$variables) {
            return;
        }
        for ($i = count($this->parent_node_list) - 1; $i >= 0; $i--) {
            $parent_node = $this->parent_node_list[$i];
            $outer_variables = [];
            switch ($parent_node->kind) {
                case ast\AST_FOREACH:
                    $outer_variables = $this->extractLoopVariablesOfForeach($parent_node);
                    break;
                case ast\AST_FOR:
                    $outer_variables = $this->extractLoopVariablesOfFor($parent_node);
                    break;
                case ast\AST_WHILE:
                    $outer_variables = $this->extractLoopVariablesOfWhile($parent_node);
                    break;

                case ast\AST_FUNC_DECL:
                case ast\AST_CLOSURE:
                case ast\AST_ARROW_FUNC:
                case ast\AST_METHOD:
                case ast\AST_CLASS:
                    return;
                default:
                    continue 2;
            }
            $common_outer_variables = array_intersect_key($outer_variables, $variables);
            if ($common_outer_variables) {
                $this->warnCommonOuterVariables($variables, $common_outer_variables);
                return;
            }
        }
    }

    /**
     * @param array<string|int,Node> $variables
     * @param array<string|int,Node> $common_outer_variables
     */
    private function warnCommonOuterVariables(array $variables, array $common_outer_variables): void
    {
        foreach ($common_outer_variables as $variable_name => $node) {
            $inner_node = $variables[$variable_name];
            $this->emitPluginIssue(
                $this->code_base,
                (clone $this->context)->withLineNumberStart($inner_node->lineno),
                'PhanPluginLoopVariableReuse',
                'Variable ${VARIABLE} used in loop was also used in an outer loop on line {LINE}',
                [$variable_name, $node->lineno]
            );
        }
    }

    /**
     * @param Node|string|int|float|null $node
     * @return array<int|string,Node> a list of all variable nodes in this foreach
     */
    public function extractVariables($node): array
    {
        if (!$node instanceof Node) {
            return [];
        }
        switch ($node->kind) {
            case ast\AST_VAR:
                if ($node->kind === ast\AST_VAR) {
                    $var_name = $node->children['name'];
                    if (is_string($var_name)) {
                        if (in_array($var_name, ['this', '_'], true) || Variable::isHardcodedVariableInScopeWithName($var_name, $this->context->isInGlobalScope())) {
                            return [];
                        }
                        return [$var_name => $node];
                    }
                }
                break;
                // Kinds of nodes we don't bother checking
            case ast\AST_STATIC_PROP:
            case ast\AST_PROP:
                // Kinds of declarations creating a new scope.
            case ast\AST_FUNC_DECL:
            case ast\AST_CLOSURE:
            case ast\AST_ARROW_FUNC:
            case ast\AST_METHOD:
            case ast\AST_CLASS:
                // FUNC_DECL and METHOD are probably unreachable.
                return [];
        }
        $result = [];
        foreach ($node->children as $child_node) {
            $result += $this->extractVariables($child_node);
        }
        return $result;
    }
}
