<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Polyfill\Intl\Grapheme;

\define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) ? '\X' : Grapheme::GRAPHEME_CLUSTER_RX);

/**
 * Partial intl implementation in pure PHP.
 *
 * Implemented:
 * - grapheme_extract  - Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8
 * - grapheme_stripos  - Find position (in grapheme units) of first occurrence of a case-insensitive string
 * - grapheme_stristr  - Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack
 * - grapheme_strlen   - Get string length in grapheme units
 * - grapheme_strpos   - Find position (in grapheme units) of first occurrence of a string
 * - grapheme_strripos - Find position (in grapheme units) of last occurrence of a case-insensitive string
 * - grapheme_strrpos  - Find position (in grapheme units) of last occurrence of a string
 * - grapheme_strstr   - Returns part of haystack string from the first occurrence of needle to the end of haystack
 * - grapheme_substr   - Return part of a string
 * - grapheme_str_split - Splits a string into an array of individual or chunks of graphemes
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
final class Grapheme
{
    // (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control])
    // This regular expression is a work around for http://bugs.exim.org/1279
    public const GRAPHEME_CLUSTER_RX = '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[Ã¡â€ Â¨-Ã¡â€¡Â¹]+|[Ã¡â€žâ‚¬-Ã¡â€¦Å¸]*(?:[ÃªÂ°â‚¬ÃªÂ°Å“ÃªÂ°Â¸ÃªÂ±â€ÃªÂ±Â°ÃªÂ²Å’ÃªÂ²Â¨ÃªÂ³â€žÃªÂ³Â ÃªÂ³Â¼ÃªÂ´ËœÃªÂ´Â´ÃªÂµÂÃªÂµÂ¬ÃªÂ¶Ë†ÃªÂ¶Â¤ÃªÂ·â‚¬ÃªÂ·Å“ÃªÂ·Â¸ÃªÂ¸â€ÃªÂ¸Â°ÃªÂ¹Å’ÃªÂ¹Â¨ÃªÂºâ€žÃªÂºÂ ÃªÂºÂ¼ÃªÂ»ËœÃªÂ»Â´ÃªÂ¼ÂÃªÂ¼Â¬ÃªÂ½Ë†ÃªÂ½Â¤ÃªÂ¾â‚¬ÃªÂ¾Å“ÃªÂ¾Â¸ÃªÂ¿â€ÃªÂ¿Â°Ã«â‚¬Å’Ã«â‚¬Â¨Ã«Ââ€žÃ«ÂÂ Ã«ÂÂ¼Ã«â€šËœÃ«â€šÂ´Ã«Æ’ÂÃ«Æ’Â¬Ã«â€žË†Ã«â€žÂ¤Ã«â€¦â‚¬Ã«â€¦Å“Ã«â€¦Â¸Ã«â€ â€Ã«â€ Â°Ã«â€¡Å’Ã«â€¡Â¨Ã«Ë†â€žÃ«Ë†Â Ã«Ë†Â¼Ã«â€°ËœÃ«â€°Â´Ã«Å ÂÃ«Å Â¬Ã«â€¹Ë†Ã«â€¹Â¤Ã«Å’â‚¬Ã«Å’Å“Ã«Å’Â¸Ã«Ââ€Ã«ÂÂ°Ã«Å½Å’Ã«Å½Â¨Ã«Ââ€žÃ«ÂÂ Ã«ÂÂ¼Ã«ÂËœÃ«ÂÂ´Ã«â€˜ÂÃ«â€˜Â¬Ã«â€™Ë†Ã«â€™Â¤Ã«â€œâ‚¬Ã«â€œÅ“Ã«â€œÂ¸Ã«â€â€Ã«â€Â°Ã«â€¢Å’Ã«â€¢Â¨Ã«â€“â€žÃ«â€“Â Ã«â€“Â¼Ã«â€”ËœÃ«â€”Â´Ã«ËœÂÃ«ËœÂ¬Ã«â„¢Ë†Ã«â„¢Â¤Ã«Å¡â‚¬Ã«Å¡Å“Ã«Å¡Â¸Ã«â€ºâ€Ã«â€ºÂ°Ã«Å“Å’Ã«Å“Â¨Ã«Ââ€žÃ«ÂÂ Ã«ÂÂ¼Ã«Å¾ËœÃ«Å¾Â´Ã«Å¸ÂÃ«Å¸Â¬Ã«Â Ë†Ã«Â Â¤Ã«Â¡â‚¬Ã«Â¡Å“Ã«Â¡Â¸Ã«Â¢â€Ã«Â¢Â°Ã«Â£Å’Ã«Â£Â¨Ã«Â¤â€žÃ«Â¤Â Ã«Â¤Â¼Ã«Â¥ËœÃ«Â¥Â´Ã«Â¦ÂÃ«Â¦Â¬Ã«Â§Ë†Ã«Â§Â¤Ã«Â¨â‚¬Ã«Â¨Å“Ã«Â¨Â¸Ã«Â©â€Ã«Â©Â°Ã«ÂªÅ’Ã«ÂªÂ¨Ã«Â«â€žÃ«Â«Â Ã«Â«Â¼Ã«Â¬ËœÃ«Â¬Â´Ã«Â­ÂÃ«Â­Â¬Ã«Â®Ë†Ã«Â®Â¤Ã«Â¯â‚¬Ã«Â¯Å“Ã«Â¯Â¸Ã«Â°â€Ã«Â°Â°Ã«Â±Å’Ã«Â±Â¨Ã«Â²â€žÃ«Â²Â Ã«Â²Â¼Ã«Â³ËœÃ«Â³Â´Ã«Â´ÂÃ«Â´Â¬Ã«ÂµË†Ã«ÂµÂ¤Ã«Â¶â‚¬Ã«Â¶Å“Ã«Â¶Â¸Ã«Â·â€Ã«Â·Â°Ã«Â¸Å’Ã«Â¸Â¨Ã«Â¹â€žÃ«Â¹Â Ã«Â¹Â¼Ã«ÂºËœÃ«ÂºÂ´Ã«Â»ÂÃ«Â»Â¬Ã«Â¼Ë†Ã«Â¼Â¤Ã«Â½â‚¬Ã«Â½Å“Ã«Â½Â¸Ã«Â¾â€Ã«Â¾Â°Ã«Â¿Å’Ã«Â¿Â¨Ã¬â‚¬â€žÃ¬â‚¬Â Ã¬â‚¬Â¼Ã¬ÂËœÃ¬ÂÂ´Ã¬â€šÂÃ¬â€šÂ¬Ã¬Æ’Ë†Ã¬Æ’Â¤Ã¬â€žâ‚¬Ã¬â€žÅ“Ã¬â€žÂ¸Ã¬â€¦â€Ã¬â€¦Â°Ã¬â€ Å’Ã¬â€ Â¨Ã¬â€¡â€žÃ¬â€¡Â Ã¬â€¡Â¼Ã¬Ë†ËœÃ¬Ë†Â´Ã¬â€°ÂÃ¬â€°Â¬Ã¬Å Ë†Ã¬Å Â¤Ã¬â€¹â‚¬Ã¬â€¹Å“Ã¬â€¹Â¸Ã¬Å’â€Ã¬Å’Â°Ã¬ÂÅ’Ã¬ÂÂ¨Ã¬Å½â€žÃ¬Å½Â Ã¬Å½Â¼Ã¬ÂËœÃ¬ÂÂ´Ã¬ÂÂÃ¬ÂÂ¬Ã¬â€˜Ë†Ã¬â€˜Â¤Ã¬â€™â‚¬Ã¬â€™Å“Ã¬â€™Â¸Ã¬â€œâ€Ã¬â€œÂ°Ã¬â€Å’Ã¬â€Â¨Ã¬â€¢â€žÃ¬â€¢Â Ã¬â€¢Â¼Ã¬â€“ËœÃ¬â€“Â´Ã¬â€”ÂÃ¬â€”Â¬Ã¬ËœË†Ã¬ËœÂ¤Ã¬â„¢â‚¬Ã¬â„¢Å“Ã¬â„¢Â¸Ã¬Å¡â€Ã¬Å¡Â°Ã¬â€ºÅ’Ã¬â€ºÂ¨Ã¬Å“â€žÃ¬Å“Â Ã¬Å“Â¼Ã¬ÂËœÃ¬ÂÂ´Ã¬Å¾ÂÃ¬Å¾Â¬Ã¬Å¸Ë†Ã¬Å¸Â¤Ã¬Â â‚¬Ã¬Â Å“Ã¬Â Â¸Ã¬Â¡â€Ã¬Â¡Â°Ã¬Â¢Å’Ã¬Â¢Â¨Ã¬Â£â€žÃ¬Â£Â Ã¬Â£Â¼Ã¬Â¤ËœÃ¬Â¤Â´Ã¬Â¥ÂÃ¬Â¥Â¬Ã¬Â¦Ë†Ã¬Â¦Â¤Ã¬Â§â‚¬Ã¬Â§Å“Ã¬Â§Â¸Ã¬Â¨â€Ã¬Â¨Â°Ã¬Â©Å’Ã¬Â©Â¨Ã¬Âªâ€žÃ¬ÂªÂ Ã¬ÂªÂ¼Ã¬Â«ËœÃ¬Â«Â´Ã¬Â¬ÂÃ¬Â¬Â¬Ã¬Â­Ë†Ã¬Â­Â¤Ã¬Â®â‚¬Ã¬Â®Å“Ã¬Â®Â¸Ã¬Â¯â€Ã¬Â¯Â°Ã¬Â°Å’Ã¬Â°Â¨Ã¬Â±â€žÃ¬Â±Â Ã¬Â±Â¼Ã¬Â²ËœÃ¬Â²Â´Ã¬Â³ÂÃ¬Â³Â¬Ã¬Â´Ë†Ã¬Â´Â¤Ã¬Âµâ‚¬Ã¬ÂµÅ“Ã¬ÂµÂ¸Ã¬Â¶â€Ã¬Â¶Â°Ã¬Â·Å’Ã¬Â·Â¨Ã¬Â¸â€žÃ¬Â¸Â Ã¬Â¸Â¼Ã¬Â¹ËœÃ¬Â¹Â´Ã¬ÂºÂÃ¬ÂºÂ¬Ã¬Â»Ë†Ã¬Â»Â¤Ã¬Â¼â‚¬Ã¬Â¼Å“Ã¬Â¼Â¸Ã¬Â½â€Ã¬Â½Â°Ã¬Â¾Å’Ã¬Â¾Â¨Ã¬Â¿â€žÃ¬Â¿Â Ã¬Â¿Â¼Ã­â‚¬ËœÃ­â‚¬Â´Ã­ÂÂÃ­ÂÂ¬Ã­â€šË†Ã­â€šÂ¤Ã­Æ’â‚¬Ã­Æ’Å“Ã­Æ’Â¸Ã­â€žâ€Ã­â€žÂ°Ã­â€¦Å’Ã­â€¦Â¨Ã­â€ â€žÃ­â€ Â Ã­â€ Â¼Ã­â€¡ËœÃ­â€¡Â´Ã­Ë†ÂÃ­Ë†Â¬Ã­â€°Ë†Ã­â€°Â¤Ã­Å â‚¬Ã­Å Å“Ã­Å Â¸Ã­â€¹â€Ã­â€¹Â°Ã­Å’Å’Ã­Å’Â¨Ã­Ââ€žÃ­ÂÂ Ã­ÂÂ¼Ã­Å½ËœÃ­Å½Â´Ã­ÂÂÃ­ÂÂ¬Ã­ÂË†Ã­ÂÂ¤Ã­â€˜â‚¬Ã­â€˜Å“Ã­â€˜Â¸Ã­â€™â€Ã­â€™Â°Ã­â€œÅ’Ã­â€œÂ¨Ã­â€â€žÃ­â€Â Ã­â€Â¼Ã­â€¢ËœÃ­â€¢Â´Ã­â€“ÂÃ­â€“Â¬Ã­â€”Ë†Ã­â€”Â¤Ã­Ëœâ‚¬Ã­ËœÅ“Ã­ËœÂ¸Ã­â„¢â€Ã­â„¢Â°Ã­Å¡Å’Ã­Å¡Â¨Ã­â€ºâ€žÃ­â€ºÂ Ã­â€ºÂ¼Ã­Å“ËœÃ­Å“Â´Ã­ÂÂÃ­ÂÂ¬Ã­Å¾Ë†]?[Ã¡â€¦Â -Ã¡â€ Â¢]+|[ÃªÂ°â‚¬-Ã­Å¾Â£])[Ã¡â€ Â¨-Ã¡â€¡Â¹]*|[Ã¡â€žâ‚¬-Ã¡â€¦Å¸]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])';

    private const CASE_FOLD = [
        ['Ã‚Âµ', 'Ã…Â¿', "\xCD\x85", 'Ãâ€š', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
        ['ÃŽÂ¼', 's', 'ÃŽÂ¹',        'ÃÆ’', 'ÃŽÂ²',        'ÃŽÂ¸',        'Ãâ€ ',        'Ãâ‚¬',        'ÃŽÂº',        'ÃÂ',        'ÃŽÂµ',        "\xE1\xB9\xA1", 'ÃŽÂ¹'],
    ];

    public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0)
    {
        if (0 > $start) {
            $start = \strlen($s) + $start;
        }

        if (!\is_scalar($s)) {
            $hasError = false;
            set_error_handler(function () use (&$hasError) { $hasError = true; });
            $next = substr($s, $start);
            restore_error_handler();
            if ($hasError) {
                substr($s, $start);
                $s = '';
            } else {
                $s = $next;
            }
        } else {
            $s = substr($s, $start);
        }
        $size = (int) $size;
        $type = (int) $type;
        $start = (int) $start;

        if (\GRAPHEME_EXTR_COUNT !== $type && \GRAPHEME_EXTR_MAXBYTES !== $type && \GRAPHEME_EXTR_MAXCHARS !== $type) {
            if (80000 > \PHP_VERSION_ID) {
                return false;
            }

            throw new \ValueError('grapheme_extract(): Argument #3 ($type) must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS');
        }

        if (!isset($s[0]) || 0 > $size || 0 > $start) {
            return false;
        }
        if (0 === $size) {
            return '';
        }

        $next = $start;

        $s = preg_split('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', "\r\n".$s, $size + 1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE);

        if (!isset($s[1])) {
            return false;
        }

        $i = 1;
        $ret = '';

        do {
            if (\GRAPHEME_EXTR_COUNT === $type) {
                --$size;
            } elseif (\GRAPHEME_EXTR_MAXBYTES === $type) {
                $size -= \strlen($s[$i]);
            } else {
                $size -= iconv_strlen($s[$i], 'UTF-8//IGNORE');
            }

            if ($size >= 0) {
                $ret .= $s[$i];
            }
        } while (isset($s[++$i]) && $size > 0);

        $next += \strlen($ret);

        return $ret;
    }

    public static function grapheme_strlen($s)
    {
        preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $s, -1, $len);

        return 0 === $len && '' !== $s ? null : $len;
    }

    public static function grapheme_substr($s, $start, $len = null)
    {
        if (null === $len) {
            $len = 2147483647;
        }

        preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $s);

        $slen = \count($s[0]);
        $start = (int) $start;

        if (0 > $start) {
            $start += $slen;
        }
        if (0 > $start) {
            if (\PHP_VERSION_ID < 80000) {
                return false;
            }

            $start = 0;
        }
        if ($start >= $slen) {
            return \PHP_VERSION_ID >= 80000 ? '' : false;
        }

        $rem = $slen - $start;

        if (0 > $len) {
            $len += $rem;
        }
        if (0 === $len) {
            return '';
        }
        if (0 > $len) {
            return \PHP_VERSION_ID >= 80000 ? '' : false;
        }
        if ($len > $rem) {
            $len = $rem;
        }

        return implode('', \array_slice($s[0], $start, $len));
    }

    public static function grapheme_strpos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 0);
    }

    public static function grapheme_stripos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 1);
    }

    public static function grapheme_strrpos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 2);
    }

    public static function grapheme_strripos($s, $needle, $offset = 0)
    {
        return self::grapheme_position($s, $needle, $offset, 3);
    }

    public static function grapheme_stristr($s, $needle, $beforeNeedle = false)
    {
        return mb_stristr($s, $needle, $beforeNeedle, 'UTF-8');
    }

    public static function grapheme_strstr($s, $needle, $beforeNeedle = false)
    {
        return mb_strstr($s, $needle, $beforeNeedle, 'UTF-8');
    }

    public static function grapheme_str_split($s, $len = 1)
    {
        if (0 > $len || 1073741823 < $len) {
            if (80000 > \PHP_VERSION_ID) {
                return false;
            }

            throw new \ValueError('grapheme_str_split(): Argument #2 ($length) must be greater than 0 and less than or equal to 1073741823.');
        }

        if ('' === $s) {
            return [];
        }

        if (!preg_match_all('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', $s, $matches)) {
            return false;
        }

        if (1 === $len) {
            return $matches[0];
        }

        $chunks = array_chunk($matches[0], $len);

        foreach ($chunks as &$chunk) {
            $chunk = implode('', $chunk);
        }

        return $chunks;
    }

    private static function grapheme_position($s, $needle, $offset, $mode)
    {
        $needle = (string) $needle;
        if (80000 > \PHP_VERSION_ID && !preg_match('/./us', $needle)) {
            return false;
        }
        $s = (string) $s;
        if (!preg_match('/./us', $s)) {
            return false;
        }
        if ($offset > 0) {
            $s = self::grapheme_substr($s, $offset);
        } elseif ($offset < 0) {
            if (2 > $mode) {
                $offset += self::grapheme_strlen($s);
                $s = self::grapheme_substr($s, $offset);
                if (0 > $offset) {
                    $offset = 0;
                }
            } elseif (0 > $offset += self::grapheme_strlen($needle)) {
                $s = self::grapheme_substr($s, 0, $offset);
                $offset = 0;
            } else {
                $offset = 0;
            }
        }

        // As UTF-8 is self-synchronizing, and we have ensured the strings are valid UTF-8,
        // we can use normal binary string functions here. For case-insensitive searches,
        // case fold the strings first.
        $caseInsensitive = $mode & 1;
        $reverse = $mode & 2;
        if ($caseInsensitive) {
            // Use the same case folding mode as mbstring does for mb_stripos().
            // Stick to SIMPLE case folding to avoid changing the length of the string, which
            // might result in offsets being shifted.
            $mode = \defined('MB_CASE_FOLD_SIMPLE') ? \MB_CASE_FOLD_SIMPLE : \MB_CASE_LOWER;
            $s = mb_convert_case($s, $mode, 'UTF-8');
            $needle = mb_convert_case($needle, $mode, 'UTF-8');

            if (!\defined('MB_CASE_FOLD_SIMPLE')) {
                $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s);
                $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle);
            }
        }
        if ($reverse) {
            $needlePos = strrpos($s, $needle);
        } else {
            $needlePos = strpos($s, $needle);
        }

        return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false;
    }
}
