protected static function graphMatch($password, $graph)
    {
        $result = [];
        $i = 0;

        $passwordLength = strlen($password);

        while ($i < $passwordLength - 1) {
            $j = $i + 1;
            $lastDirection = null;
            $turns = 0;
            $shiftedCount = 0;

            while (true) {
                $prevChar = $password[$j - 1];
                $found = false;
                $curDirection = -1;
                $adjacents = isset($graph[$prevChar]) ? $graph[$prevChar] : [];
                // Consider growing pattern by one character if j hasn't gone over the edge.
                if ($j < $passwordLength) {
                    $curChar = $password[$j];
                    foreach ($adjacents as $adj) {
                        ++$curDirection;
                        $curCharPos = static::indexOf($adj, $curChar);
                        if ($adj && $curCharPos !== -1) {
                            $found = true;
                            $foundDirection = $curDirection;

                            if (1 === $curCharPos) {
                                // index 1 in the adjacency means the key is shifted, 0 means unshifted: A vs a, % vs 5, etc.
                                // for example, 'q' is adjacent to the entry '2@'. @ is shifted w/ index 1, 2 is unshifted.
                                ++$shiftedCount;
                            }
                            if ($lastDirection !== $foundDirection) {
                                // adding a turn is correct even in the initial case when last_direction is null:
                                // every spatial pattern starts with a turn.
                                ++$turns;
                                $lastDirection = $foundDirection;
                            }

                            break;
                        }
                    }
                }

                // if the current pattern continued, extend j and try to grow again
                if ($found) {
                    ++$j;
                }
                // otherwise push the pattern discovered so far, if any...
                else {
                    // Ignore length 1 or 2 chains.
                    if ($j - $i > 2) {
                        $result[] = [
                            'begin' => $i,
                            'end' => $j - 1,
                            'token' => substr($password, $i, $j - $i),
                            'turns' => $turns,
                            'shifted_count' => $shiftedCount,
                        ];
                    }
                    // ...and then start a new search for the rest of the password.
                    $i = $j;

                    break;
                }
            }
        }

        return $result;
    }