<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer;

use PhpCsFixer\Tokenizer\Tokens;

/**
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
abstract class AbstractNoUselessElseFixer extends AbstractFixer
{
    public function getPriority(): int
    {
        // should be run before NoWhitespaceInBlankLineFixer, NoExtraBlankLinesFixer, BracesFixer and after NoEmptyStatementFixer.
        return 39;
    }

    protected function isSuperfluousElse(Tokens $tokens, int $index): bool
    {
        $previousBlockStart = $index;

        do {
            // Check if all 'if', 'else if ' and 'elseif' blocks above this 'else' always end,
            // if so this 'else' is overcomplete.
            [$previousBlockStart, $previousBlockEnd] = $this->getPreviousBlock($tokens, $previousBlockStart);

            // short 'if' detection
            $previous = $previousBlockEnd;
            if ($tokens[$previous]->equals('}')) {
                $previous = $tokens->getPrevMeaningfulToken($previous);
            }

            if (
                !$tokens[$previous]->equals(';')                                    // 'if' block doesn't end with semicolon, keep 'else'
                || $tokens[$tokens->getPrevMeaningfulToken($previous)]->equals('{') // empty 'if' block, keep 'else'
            ) {
                return false;
            }

            $candidateIndex = $tokens->getPrevTokenOfKind(
                $previous,
                [
                    ';',
                    [\T_BREAK],
                    [\T_CLOSE_TAG],
                    [\T_CONTINUE],
                    [\T_EXIT],
                    [\T_GOTO],
                    [\T_IF],
                    [\T_RETURN],
                    [\T_THROW],
                ],
            );

            if (null === $candidateIndex || $tokens[$candidateIndex]->equalsAny([';', [\T_CLOSE_TAG], [\T_IF]])) {
                return false;
            }

            if ($tokens[$candidateIndex]->isGivenKind(\T_THROW)) {
                $previousIndex = $tokens->getPrevMeaningfulToken($candidateIndex);

                if (!$tokens[$previousIndex]->equalsAny([';', '{'])) {
                    return false;
                }
            }

            if ($this->isInConditional($tokens, $candidateIndex, $previousBlockStart)
                || $this->isInConditionWithoutBraces($tokens, $candidateIndex, $previousBlockStart)
            ) {
                return false;
            }

            // implicit continue, i.e. delete candidate
        } while (!$tokens[$previousBlockStart]->isGivenKind(\T_IF));

        return true;
    }

    /**
     * Return the first and last token index of the previous block.
     *
     * [0] First is either T_IF, T_ELSE or T_ELSEIF
     * [1] Last is either '}' or ';' / T_CLOSE_TAG for short notation blocks
     *
     * @param int $index T_IF, T_ELSE, T_ELSEIF
     *
     * @return array{int, int}
     */
    private function getPreviousBlock(Tokens $tokens, int $index): array
    {
        $close = $previous = $tokens->getPrevMeaningfulToken($index);
        // short 'if' detection
        if ($tokens[$close]->equals('}')) {
            $previous = $tokens->findBlockStart(Tokens::BLOCK_TYPE_BRACE, $close);
        }

        $open = $tokens->getPrevTokenOfKind($previous, [[\T_IF], [\T_ELSE], [\T_ELSEIF]]);
        if ($tokens[$open]->isGivenKind(\T_IF)) {
            $elseCandidate = $tokens->getPrevMeaningfulToken($open);
            if ($tokens[$elseCandidate]->isGivenKind(\T_ELSE)) {
                $open = $elseCandidate;
            }
        }

        return [$open, $close];
    }

    /**
     * @param int $index           Index of the token to check
     * @param int $lowerLimitIndex Lower limit index. Since the token to check will always be in a conditional we must stop checking at this index
     */
    private function isInConditional(Tokens $tokens, int $index, int $lowerLimitIndex): bool
    {
        $candidateIndex = $tokens->getPrevTokenOfKind($index, [')', ';', ':']);
        if ($tokens[$candidateIndex]->equals(':')) {
            return true;
        }

        if (!$tokens[$candidateIndex]->equals(')')) {
            return false; // token is ';' or close tag
        }

        // token is always ')' here.
        // If it is part of the condition the token is always in, return false.
        // If it is not it is a nested condition so return true
        $open = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS, $candidateIndex);

        return $tokens->getPrevMeaningfulToken($open) > $lowerLimitIndex;
    }

    /**
     * For internal use only, as it is not perfect.
     *
     * Returns if the token at given index is part of an if/elseif/else statement
     * without {}. Assumes not passing the last `;`/close tag of the statement, not
     * out of range index, etc.
     *
     * @param int $index Index of the token to check
     */
    private function isInConditionWithoutBraces(Tokens $tokens, int $index, int $lowerLimitIndex): bool
    {
        do {
            if ($tokens[$index]->isComment() || $tokens[$index]->isWhitespace()) {
                $index = $tokens->getPrevMeaningfulToken($index);
            }

            $token = $tokens[$index];
            if ($token->isGivenKind([\T_IF, \T_ELSEIF, \T_ELSE])) {
                return true;
            }

            if ($token->equals(';')) {
                return false;
            }

            if ($token->equals('{')) {
                $index = $tokens->getPrevMeaningfulToken($index);

                // OK if belongs to: for, do, while, foreach
                // Not OK if belongs to: if, else, elseif
                if ($tokens[$index]->isGivenKind(\T_DO)) {
                    --$index;

                    continue;
                }

                if (!$tokens[$index]->equals(')')) {
                    return false; // like `else {`
                }

                $index = $tokens->findBlockStart(
                    Tokens::BLOCK_TYPE_PARENTHESIS,
                    $index,
                );

                $index = $tokens->getPrevMeaningfulToken($index);
                if ($tokens[$index]->isGivenKind([\T_IF, \T_ELSEIF])) {
                    return false;
                }
            } elseif ($token->equals(')')) {
                $type = Tokens::detectBlockType($token);
                $index = $tokens->findBlockStart(
                    $type['type'],
                    $index,
                );

                $index = $tokens->getPrevMeaningfulToken($index);
            } else {
                --$index;
            }
        } while ($index > $lowerLimitIndex);

        return false;
    }
}
