<?php
/**
 * @license MIT
 *
 * Modified using Strauss.
 * @see https://github.com/BrianHenryIE/strauss
 */

declare(strict_types=1);

namespace iThemesSecurity\Strauss\ZxcvbnPhp\Matchers;

use JetBrains\PhpStorm\ArrayShape;
use iThemesSecurity\Strauss\ZxcvbnPhp\Matcher;
use iThemesSecurity\Strauss\ZxcvbnPhp\Scorer;

class RepeatMatch extends BaseMatch
{
    public const GREEDY_MATCH = '/(.+)\1+/u';
    public const LAZY_MATCH = '/(.+?)\1+/u';
    public const ANCHORED_LAZY_MATCH = '/^(.+?)\1+$/u';

    public $pattern = 'repeat';

    /** @var MatchInterface[] An array of matches for the repeated section itself. */
    public $baseMatches = [];

    /** @var int The number of guesses required for the repeated section itself. */
    public $baseGuesses;

    /** @var int The number of times the repeated section is repeated. */
    public $repeatCount;

    /** @var string The string that was repeated in the token. */
    public $repeatedChar;

    /**
     * Match 3 or more repeated characters.
     *
     * @param string $password
     * @param array $userInputs
     * @return RepeatMatch[]
     */
    public static function match(string $password, array $userInputs = []): array
    {
        $matches = [];
        $lastIndex = 0;

        while ($lastIndex < mb_strlen($password)) {
            $greedyMatches = self::findAll($password, self::GREEDY_MATCH, $lastIndex);
            $lazyMatches = self::findAll($password, self::LAZY_MATCH, $lastIndex);

            if (empty($greedyMatches)) {
                break;
            }

            if (mb_strlen($greedyMatches[0][0]['token']) > mb_strlen($lazyMatches[0][0]['token'])) {
                $match = $greedyMatches[0];
                preg_match(self::ANCHORED_LAZY_MATCH, $match[0]['token'], $anchoredMatch);
                $repeatedChar = $anchoredMatch[1];
            } else {
                $match = $lazyMatches[0];
                $repeatedChar = $match[1]['token'];
            }

            $scorer = new Scorer();
            $matcher = new Matcher();

            $baseAnalysis = $scorer->getMostGuessableMatchSequence($repeatedChar, $matcher->getMatches($repeatedChar));
            $baseMatches = $baseAnalysis['sequence'];
            $baseGuesses = $baseAnalysis['guesses'];

            $repeatCount = mb_strlen($match[0]['token']) / mb_strlen($repeatedChar);

            $matches[] = new static(
                $password,
                $match[0]['begin'],
                $match[0]['end'],
                $match[0]['token'],
                [
                    'repeated_char' => $repeatedChar,
                    'base_guesses'  => $baseGuesses,
                    'base_matches'  => $baseMatches,
                    'repeat_count'  => $repeatCount,
                ]
            );

            $lastIndex = $match[0]['end'] + 1;
        }

        return $matches;
    }

    #[ArrayShape(['warning' => 'string', 'suggestions' => 'string[]'])]
    public function getFeedback(bool $isSoleMatch): array
    {
        $warning = mb_strlen($this->repeatedChar) == 1
            ? 'Repeats like "aaa" are easy to guess'
            : 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"';

        return [
            'warning'     => $warning,
            'suggestions' => [
                'Avoid repeated words and characters',
            ],
        ];
    }

    /**
     * @param string $password
     * @param int $begin
     * @param int $end
     * @param string $token
     * @param array $params An array with keys: [repeated_char, base_guesses, base_matches, repeat_count].
     */
    public function __construct(string $password, int $begin, int $end, string $token, array $params = [])
    {
        parent::__construct($password, $begin, $end, $token);
        if (!empty($params)) {
            $this->repeatedChar = $params['repeated_char'] ?? '';
            $this->baseGuesses = $params['base_guesses'] ?? 0;
            $this->baseMatches = $params['base_matches'] ?? [];
            $this->repeatCount = $params['repeat_count'] ?? 0;
        }
    }

    protected function getRawGuesses(): float
    {
        return $this->baseGuesses * $this->repeatCount;
    }
}
