| 
<?php
 namespace Fuse\Search\Bitap;
 
 use Fuse\Exception\PatternLengthTooLargeException;
 use Fuse\Search\Bitap\Constants;
 
 use function Fuse\Search\Bitap\computeScore;
 use function Fuse\Search\Bitap\convertMaskToIndices;
 use function Fuse\Core\config;
 
 /**
 * @return (array|bool|float|int|mixed)[]
 */
 function search(string $text, string $pattern, array $patternAlphabet, array $options = []): array
 {
 $location = $options['location'] ?? config('location');
 $distance = $options['distance'] ?? config('distance');
 $threshold = $options['threshold'] ?? config('threshold');
 $findAllMatches = $options['findAllMatches'] ?? config('findAllMatches');
 $minMatchCharLength = $options['minMatchCharLength'] ?? config('minMatchCharLength');
 $includeMatches = $options['includeMatches'] ?? config('includeMatches');
 $ignoreLocation = $options['ignoreLocation'] ?? config('ignoreLocation');
 
 if (mb_strlen($pattern) > Constants::MAX_BITS) {
 throw new PatternLengthTooLargeException(Constants::MAX_BITS);
 }
 
 $patternLen = mb_strlen($pattern);
 
 // Set starting location at beginning text and initialize the alphabet.
 $textLen = mb_strlen($text);
 
 // Handle the case when location > text.length
 $expectedLocation = max(0, min($location, $textLen));
 
 // Highest score beyond which we give up.
 $currentThreshold = $threshold;
 
 // Is there a nearby exact match? (speedup)
 $bestLocation = $expectedLocation;
 
 // Performance: only computer matches when the minMatchCharLength > 1
 // OR if `includeMatches` is true.
 $computeMatches = $minMatchCharLength > 1 || $includeMatches;
 
 // A mask of the matches, used for building the indices
 $matchMask = $computeMatches ? [$textLen] : [];
 
 // Get all exact matches, here for speed up
 while (($index = mb_strpos($text, $pattern, $bestLocation)) !== false) {
 $score = computeScore($pattern, [
 'currentLocation' => $index,
 'expectedLocation' => $expectedLocation,
 'distance' => $distance,
 'ignoreLocation' => $ignoreLocation,
 ]);
 
 $currentThreshold = min($score, $currentThreshold);
 $bestLocation = $index + $patternLen;
 
 if ($computeMatches) {
 $i = 0;
 while ($i < $patternLen) {
 $matchMask[$index + $i] = 1;
 $i += 1;
 }
 }
 }
 
 // Reset the best location
 $bestLocation = -1;
 
 $lastBitArr = [];
 $finalScore = 1;
 $binMax = $patternLen + $textLen;
 
 $mask = 1 << $patternLen - 1;
 
 for ($i = 0; $i < $patternLen; $i += 1) {
 // Scan for the best match; each iteration allows for one more error.
 // Run a binary search to determine how far from the match location we can stray
 // at this error level.
 $binMin = 0;
 $binMid = $binMax;
 
 while ($binMin < $binMid) {
 $score = computeScore($pattern, [
 'errors' => $i,
 'currentLocation' => $expectedLocation + $binMid,
 'expectedLocation' => $expectedLocation,
 'distance' => $distance,
 'ignoreLocation' => $ignoreLocation,
 ]);
 
 if ($score <= $currentThreshold) {
 $binMin = $binMid;
 } else {
 $binMax = $binMid;
 }
 
 $binMid = floor(($binMax - $binMin) / 2 + $binMin);
 }
 
 // Use the result from this iteration as the maximum for the next.
 $binMax = $binMid;
 
 $start = max(1, $expectedLocation - $binMid + 1);
 $finish = $findAllMatches
 ? $textLen
 : min($expectedLocation + $binMid, $textLen) + $patternLen;
 
 // Initialize the bit array
 $bitArr = [$finish + 2];
 
 $bitArr[$finish + 1] = (1 << $i) - 1;
 
 for ($j = $finish; $j >= $start; $j -= 1) {
 $currentLocation = $j - 1;
 $charMatch = $patternAlphabet[mb_substr($text, $currentLocation, 1)] ?? null;
 
 if ($computeMatches) {
 // Speed up: quick bool to int conversion (i.e, `charMatch ? 1 : 0`)
 $matchMask[$currentLocation] = (int) $charMatch;
 }
 
 // First pass: exact match
 $bitArr[$j] = ((($bitArr[$j + 1] ?? 0) << 1) | 1) & $charMatch;
 
 // Subsequent passes: fuzzy match
 if ($i) {
 $bitArr[$j] |=
 ((($lastBitArr[$j + 1] ?? 0) | ($lastBitArr[$j] ?? 0)) << 1) |
 1 |
 ($lastBitArr[$j + 1] ?? 0);
 }
 
 if ($bitArr[$j] & $mask) {
 $finalScore = computeScore($pattern, [
 'errors' => $i,
 'currentLocation' => $currentLocation,
 'expectedLocation' => $expectedLocation,
 'distance' => $distance,
 'ignoreLocation' => $ignoreLocation,
 ]);
 
 // This match will almost certainly be better than any existing match.
 // But check anyway.
 if ($finalScore <= $currentThreshold) {
 // Indeed it is
 $currentThreshold = $finalScore;
 $bestLocation = $currentLocation;
 
 // Already passed `loc`, downhill from here on in.
 if ($bestLocation <= $expectedLocation) {
 break;
 }
 
 // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.
 $start = max(1, 2 * $expectedLocation - $bestLocation);
 }
 }
 }
 
 // No hope for a (better) match at greater error levels.
 $score = computeScore($pattern, [
 'errors' => $i + 1,
 'currentLocation' => $expectedLocation,
 'expectedLocation' => $expectedLocation,
 'distance' => $distance,
 'ignoreLocation' => $ignoreLocation,
 ]);
 
 if ($score > $currentThreshold) {
 break;
 }
 
 $lastBitArr = $bitArr;
 }
 
 $result = [
 'isMatch' => $bestLocation >= 0,
 'score' => max(0.001, $finalScore),
 ];
 
 if ($computeMatches) {
 $indices = convertMaskToIndices($matchMask, $minMatchCharLength);
 if (sizeof($indices) === 0) {
 $result['isMatch'] = false;
 } elseif ($includeMatches) {
 $result['indices'] = $indices;
 }
 }
 
 return $result;
 }
 
 |