����JFIF��x�x����'403WebShell
403Webshell
Server IP : 66.29.137.217  /  Your IP : 3.129.73.179
Web Server : LiteSpeed
System : Linux premium294.web-hosting.com 4.18.0-513.11.1.lve.el8.x86_64 #1 SMP Thu Jan 18 16:21:02 UTC 2024 x86_64
User : gltevjme ( 1095)
PHP Version : 7.0.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/gltevjme/cbt.gltechlimited.com/vendor/voku/portable-utf8/src/voku/helper/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/gltevjme/cbt.gltechlimited.com/vendor/voku/portable-utf8/src/voku/helper/UTF8.php
<?php

declare(strict_types=1);

namespace voku\helper;

/**
 * @psalm-immutable
 */
final class UTF8
{
    /**
     * Bom => Byte-Length
     *
     * INFO: https://en.wikipedia.org/wiki/Byte_order_mark
     *
     * @var array<string, int>
     */
    private static $BOM = [
        "\xef\xbb\xbf"     => 3, // UTF-8 BOM
        ''              => 6, // UTF-8 BOM as "WINDOWS-1252" (one char has [maybe] more then one byte ...)
        "\x00\x00\xfe\xff" => 4, // UTF-32 (BE) BOM
        '  þÿ'             => 6, // UTF-32 (BE) BOM as "WINDOWS-1252"
        "\xff\xfe\x00\x00" => 4, // UTF-32 (LE) BOM
        'ÿþ  '             => 6, // UTF-32 (LE) BOM as "WINDOWS-1252"
        "\xfe\xff"         => 2, // UTF-16 (BE) BOM
        'þÿ'               => 4, // UTF-16 (BE) BOM as "WINDOWS-1252"
        "\xff\xfe"         => 2, // UTF-16 (LE) BOM
        'ÿþ'               => 4, // UTF-16 (LE) BOM as "WINDOWS-1252"
    ];

    /**
     * Numeric code point => UTF-8 Character
     *
     * url: http://www.w3schools.com/charsets/ref_utf_punctuation.asp
     *
     * @var array<int, string>
     */
    private static $WHITESPACE = [
        // NULL Byte
        0 => "\x0",
        // Tab
        9 => "\x9",
        // New Line
        10 => "\xa",
        // Vertical Tab
        11 => "\xb",
        // Carriage Return
        13 => "\xd",
        // Ordinary Space
        32 => "\x20",
        // NO-BREAK SPACE
        160 => "\xc2\xa0",
        // OGHAM SPACE MARK
        5760 => "\xe1\x9a\x80",
        // MONGOLIAN VOWEL SEPARATOR
        6158 => "\xe1\xa0\x8e",
        // EN QUAD
        8192 => "\xe2\x80\x80",
        // EM QUAD
        8193 => "\xe2\x80\x81",
        // EN SPACE
        8194 => "\xe2\x80\x82",
        // EM SPACE
        8195 => "\xe2\x80\x83",
        // THREE-PER-EM SPACE
        8196 => "\xe2\x80\x84",
        // FOUR-PER-EM SPACE
        8197 => "\xe2\x80\x85",
        // SIX-PER-EM SPACE
        8198 => "\xe2\x80\x86",
        // FIGURE SPACE
        8199 => "\xe2\x80\x87",
        // PUNCTUATION SPACE
        8200 => "\xe2\x80\x88",
        // THIN SPACE
        8201 => "\xe2\x80\x89",
        // HAIR SPACE
        8202 => "\xe2\x80\x8a",
        // LINE SEPARATOR
        8232 => "\xe2\x80\xa8",
        // PARAGRAPH SEPARATOR
        8233 => "\xe2\x80\xa9",
        // NARROW NO-BREAK SPACE
        8239 => "\xe2\x80\xaf",
        // MEDIUM MATHEMATICAL SPACE
        8287 => "\xe2\x81\x9f",
        // HALFWIDTH HANGUL FILLER
        65440 => "\xef\xbe\xa0",
        // IDEOGRAPHIC SPACE
        12288 => "\xe3\x80\x80",
    ];

    /**
     * @var array<string, string>
     */
    private static $WHITESPACE_TABLE = [
        'SPACE'                     => "\x20",
        'NO-BREAK SPACE'            => "\xc2\xa0",
        'OGHAM SPACE MARK'          => "\xe1\x9a\x80",
        'EN QUAD'                   => "\xe2\x80\x80",
        'EM QUAD'                   => "\xe2\x80\x81",
        'EN SPACE'                  => "\xe2\x80\x82",
        'EM SPACE'                  => "\xe2\x80\x83",
        'THREE-PER-EM SPACE'        => "\xe2\x80\x84",
        'FOUR-PER-EM SPACE'         => "\xe2\x80\x85",
        'SIX-PER-EM SPACE'          => "\xe2\x80\x86",
        'FIGURE SPACE'              => "\xe2\x80\x87",
        'PUNCTUATION SPACE'         => "\xe2\x80\x88",
        'THIN SPACE'                => "\xe2\x80\x89",
        'HAIR SPACE'                => "\xe2\x80\x8a",
        'LINE SEPARATOR'            => "\xe2\x80\xa8",
        'PARAGRAPH SEPARATOR'       => "\xe2\x80\xa9",
        'ZERO WIDTH SPACE'          => "\xe2\x80\x8b",
        'NARROW NO-BREAK SPACE'     => "\xe2\x80\xaf",
        'MEDIUM MATHEMATICAL SPACE' => "\xe2\x81\x9f",
        'IDEOGRAPHIC SPACE'         => "\xe3\x80\x80",
        'HALFWIDTH HANGUL FILLER'   => "\xef\xbe\xa0",
    ];

    /**
     * @var array
     *
     * @phpstan-var array{upper: string[], lower: string[]}
     */
    private static $COMMON_CASE_FOLD = [
        'upper' => [
            'µ',
            'ſ',
            "\xCD\x85",
            'ς',
            'ẞ',
            "\xCF\x90",
            "\xCF\x91",
            "\xCF\x95",
            "\xCF\x96",
            "\xCF\xB0",
            "\xCF\xB1",
            "\xCF\xB5",
            "\xE1\xBA\x9B",
            "\xE1\xBE\xBE",
        ],
        'lower' => [
            'μ',
            's',
            'ι',
            'σ',
            'ß',
            'β',
            'θ',
            'φ',
            'π',
            'κ',
            'ρ',
            'ε',
            "\xE1\xB9\xA1",
            'ι',
        ],
    ];

    /**
     * @var array
     *
     * @phpstan-var array<string, mixed>
     */
    private static $SUPPORT = [];

    /**
     * @var string[]|null
     *
     * @phpstan-var array<string, string>|null
     */
    private static $BROKEN_UTF8_FIX;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<int, string>|null
     */
    private static $WIN1252_TO_UTF8;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<int ,string>|null
     */
    private static $INTL_TRANSLITERATOR_LIST;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<string>|null
     */
    private static $ENCODINGS;

    /**
     * @var int[]|null
     *
     * @phpstan-var array<string ,int>|null
     */
    private static $ORD;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<string, string>|null
     */
    private static $EMOJI;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<string>|null
     */
    private static $EMOJI_VALUES_CACHE;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<string>|null
     */
    private static $EMOJI_KEYS_CACHE;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<string>|null
     */
    private static $EMOJI_KEYS_REVERSIBLE_CACHE;

    /**
     * @var string[]|null
     *
     * @phpstan-var array<int, string>|null
     */
    private static $CHR;

    /**
     * __construct()
     */
    public function __construct()
    {
    }

    /**
     * Return the character at the specified position: $str[1] like functionality.
     *
     * EXAMPLE: <code>UTF8::access('fòô', 1); // 'ò'</code>
     *
     * @param string $str      <p>A UTF-8 string.</p>
     * @param int    $pos      <p>The position of character to return.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Single multi-byte character.</p>
     */
    public static function access(string $str, int $pos, string $encoding = 'UTF-8'): string
    {
        if ($str === '' || $pos < 0) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr($str, $pos, 1);
        }

        return (string) self::substr($str, $pos, 1, $encoding);
    }

    /**
     * Prepends UTF-8 BOM character to the string and returns the whole string.
     *
     * INFO: If BOM already existed there, the Input string is returned.
     *
     * EXAMPLE: <code>UTF8::add_bom_to_string('fòô'); // "\xEF\xBB\xBF" . 'fòô'</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The output string that contains BOM.</p>
     */
    public static function add_bom_to_string(string $str): string
    {
        if (!self::string_has_bom($str)) {
            $str = self::bom() . $str;
        }

        return $str;
    }

    /**
     * Changes all keys in an array.
     *
     * @param array<string, mixed> $array    <p>The array to work on</p>
     * @param int                  $case     [optional] <p> Either <strong>CASE_UPPER</strong><br>
     *                                       or <strong>CASE_LOWER</strong> (default)</p>
     * @param string               $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string[]
     *                  <p>An array with its keys lower- or uppercased.</p>
     */
    public static function array_change_key_case(
        array $array,
        int $case = \CASE_LOWER,
        string $encoding = 'UTF-8'
    ): array {
        if (
            $case !== \CASE_LOWER
            &&
            $case !== \CASE_UPPER
        ) {
            $case = \CASE_LOWER;
        }

        $return = [];
        foreach ($array as $key => &$value) {
            $key = $case === \CASE_LOWER
                ? self::strtolower($key, $encoding)
                : self::strtoupper($key, $encoding);

            $return[$key] = $value;
        }

        return $return;
    }

    /**
     * Returns the substring between $start and $end, if found, or an empty
     * string. An optional offset may be supplied from which to begin the
     * search for the start string.
     *
     * @param string $str
     * @param string $start    <p>Delimiter marking the start of the substring.</p>
     * @param string $end      <p>Delimiter marking the end of the substring.</p>
     * @param int    $offset   [optional] <p>Index from which to begin the search. Default: 0</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function between(
        string $str,
        string $start,
        string $end,
        int $offset = 0,
        string $encoding = 'UTF-8'
    ): string {
        if ($encoding === 'UTF-8') {
            $start_position = \mb_strpos($str, $start, $offset);
            if ($start_position === false) {
                return '';
            }

            $substr_index = $start_position + (int) \mb_strlen($start);
            $end_position = \mb_strpos($str, $end, $substr_index);
            if (
                $end_position === false
                ||
                $end_position === $substr_index
            ) {
                return '';
            }

            return (string) \mb_substr($str, $substr_index, $end_position - $substr_index);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        $start_position = self::strpos($str, $start, $offset, $encoding);
        if ($start_position === false) {
            return '';
        }

        $substr_index = $start_position + (int) self::strlen($start, $encoding);
        $end_position = self::strpos($str, $end, $substr_index, $encoding);
        if (
            $end_position === false
            ||
            $end_position === $substr_index
        ) {
            return '';
        }

        return (string) self::substr(
            $str,
            $substr_index,
            $end_position - $substr_index,
            $encoding
        );
    }

    /**
     * Convert binary into a string.
     *
     * INFO: opposite to UTF8::str_to_binary()
     *
     * EXAMPLE: <code>UTF8::binary_to_str('11110000100111111001100010000011'); // '😃'</code>
     *
     * @param string $bin 1|0
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function binary_to_str($bin): string
    {
        if (!isset($bin[0])) {
            return '';
        }

        $convert = \base_convert($bin, 2, 16);
        if ($convert === '0') {
            return '';
        }

        return \pack('H*', $convert);
    }

    /**
     * Returns the UTF-8 Byte Order Mark Character.
     *
     * INFO: take a look at UTF8::$bom for e.g. UTF-16 and UTF-32 BOM values
     *
     * EXAMPLE: <code>UTF8::bom(); // "\xEF\xBB\xBF"</code>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>UTF-8 Byte Order Mark.</p>
     */
    public static function bom(): string
    {
        return "\xef\xbb\xbf";
    }

    /**
     * @alias of UTF8::chr_map()
     *
     * @param callable $callback
     * @param string   $str
     *
     * @psalm-pure
     *
     * @return string[]
     *
     * @see   UTF8::chr_map()
     */
    public static function callback($callback, string $str): array
    {
        return self::chr_map($callback, $str);
    }

    /**
     * Returns the character at $index, with indexes starting at 0.
     *
     * @param string $str      <p>The input string.</p>
     * @param int    $index    <p>Position of the character.</p>
     * @param string $encoding [optional] <p>Default is UTF-8</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The character at $index.</p>
     */
    public static function char_at(string $str, int $index, string $encoding = 'UTF-8'): string
    {
        if ($encoding === 'UTF-8') {
            return (string) \mb_substr($str, $index, 1);
        }

        return (string) self::substr($str, $index, 1, $encoding);
    }

    /**
     * Returns an array consisting of the characters in the string.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return string[]
     *                  <p>An array of chars.</p>
     */
    public static function chars(string $str): array
    {
        /** @var string[] */
        return self::str_split($str);
    }

    /**
     * This method will auto-detect your server environment for UTF-8 support.
     *
     * @return true|null
     *
     * @internal <p>You don't need to run it manually, it will be triggered if it's needed.</p>
     */
    public static function checkForSupport()
    {
        if (!isset(self::$SUPPORT['already_checked_via_portable_utf8'])) {
            self::$SUPPORT['already_checked_via_portable_utf8'] = true;

            // http://php.net/manual/en/book.mbstring.php
            self::$SUPPORT['mbstring'] = self::mbstring_loaded();

            self::$SUPPORT['mbstring_func_overload'] = self::mbstring_overloaded();
            if (self::$SUPPORT['mbstring'] === true) {
                \mb_internal_encoding('UTF-8');
                \mb_regex_encoding('UTF-8');
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
            }

            // http://php.net/manual/en/book.iconv.php
            self::$SUPPORT['iconv'] = self::iconv_loaded();

            // http://php.net/manual/en/book.intl.php
            self::$SUPPORT['intl'] = self::intl_loaded();

            // http://php.net/manual/en/class.intlchar.php
            self::$SUPPORT['intlChar'] = self::intlChar_loaded();

            // http://php.net/manual/en/book.ctype.php
            self::$SUPPORT['ctype'] = self::ctype_loaded();

            // http://php.net/manual/en/class.finfo.php
            self::$SUPPORT['finfo'] = self::finfo_loaded();

            // http://php.net/manual/en/book.json.php
            self::$SUPPORT['json'] = self::json_loaded();

            // http://php.net/manual/en/book.pcre.php
            self::$SUPPORT['pcre_utf8'] = self::pcre_utf8_support();

            self::$SUPPORT['symfony_polyfill_used'] = self::symfony_polyfill_used();
            if (self::$SUPPORT['symfony_polyfill_used'] === true) {
                \mb_internal_encoding('UTF-8');
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
            }

            return true;
        }

        return null;
    }

    /**
     * Generates a UTF-8 encoded character from the given code point.
     *
     * INFO: opposite to UTF8::ord()
     *
     * EXAMPLE: <code>UTF8::chr(0x2603); // '☃'</code>
     *
     * @param int    $code_point <p>The code point for which to generate a character.</p>
     * @param string $encoding   [optional] <p>Default is UTF-8</p>
     *
     * @psalm-pure
     *
     * @return string|null
     *                     <p>Multi-byte character, returns null on failure or empty input.</p>
     */
    public static function chr($code_point, string $encoding = 'UTF-8')
    {
        // init
        /**
         * @psalm-suppress ImpureStaticVariable
         *
         * @var array<string,string>
         */
        static $CHAR_CACHE = [];

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if (
            $encoding !== 'UTF-8'
            &&
            $encoding !== 'ISO-8859-1'
            &&
            $encoding !== 'WINDOWS-1252'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::chr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        if (!\is_int($code_point) || $code_point <= 0) {
            return null;
        }

        $cache_key = $code_point . '_' . $encoding;
        if (isset($CHAR_CACHE[$cache_key])) {
            return $CHAR_CACHE[$cache_key];
        }

        if ($code_point <= 0x80) { // only for "simple"-chars

            if (self::$CHR === null) {
                self::$CHR = self::getData('chr');
            }

            /**
             * @psalm-suppress PossiblyNullArrayAccess
             */
            $chr = self::$CHR[$code_point];

            if ($encoding !== 'UTF-8') {
                $chr = self::encode($encoding, $chr);
            }

            return $CHAR_CACHE[$cache_key] = $chr;
        }

        //
        // fallback via "IntlChar"
        //

        if (self::$SUPPORT['intlChar'] === true) {
            $chr = \IntlChar::chr($code_point);

            if ($encoding !== 'UTF-8') {
                $chr = self::encode($encoding, $chr);
            }

            return $CHAR_CACHE[$cache_key] = $chr;
        }

        //
        // fallback via vanilla php
        //

        if (self::$CHR === null) {
            self::$CHR = self::getData('chr');
        }

        $code_point = (int) $code_point;
        if ($code_point <= 0x7FF) {
            /**
             * @psalm-suppress PossiblyNullArrayAccess
             */
            $chr = self::$CHR[($code_point >> 6) + 0xC0] .
                   self::$CHR[($code_point & 0x3F) + 0x80];
        } elseif ($code_point <= 0xFFFF) {
            /**
             * @psalm-suppress PossiblyNullArrayAccess
             */
            $chr = self::$CHR[($code_point >> 12) + 0xE0] .
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
                   self::$CHR[($code_point & 0x3F) + 0x80];
        } else {
            /**
             * @psalm-suppress PossiblyNullArrayAccess
             */
            $chr = self::$CHR[($code_point >> 18) + 0xF0] .
                   self::$CHR[(($code_point >> 12) & 0x3F) + 0x80] .
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
                   self::$CHR[($code_point & 0x3F) + 0x80];
        }

        if ($encoding !== 'UTF-8') {
            $chr = self::encode($encoding, $chr);
        }

        return $CHAR_CACHE[$cache_key] = $chr;
    }

    /**
     * Applies callback to all characters of a string.
     *
     * EXAMPLE: <code>UTF8::chr_map([UTF8::class, 'strtolower'], 'Κόσμε'); // ['κ','ό', 'σ', 'μ', 'ε']</code>
     *
     * @param callable $callback <p>The callback function.</p>
     * @param string   $str      <p>UTF-8 string to run callback on.</p>
     *
     * @psalm-pure
     *
     * @return string[]
     *                  <p>The outcome of the callback, as array.</p>
     */
    public static function chr_map($callback, string $str): array
    {
        return \array_map(
            $callback,
            self::str_split($str)
        );
    }

    /**
     * Generates an array of byte length of each character of a Unicode string.
     *
     * 1 byte => U+0000  - U+007F
     * 2 byte => U+0080  - U+07FF
     * 3 byte => U+0800  - U+FFFF
     * 4 byte => U+10000 - U+10FFFF
     *
     * EXAMPLE: <code>UTF8::chr_size_list('中文空白-test'); // [3, 3, 3, 3, 1, 1, 1, 1, 1]</code>
     *
     * @param string $str <p>The original unicode string.</p>
     *
     * @psalm-pure
     *
     * @return int[]
     *               <p>An array of byte lengths of each character.</p>
     */
    public static function chr_size_list(string $str): array
    {
        if ($str === '') {
            return [];
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            return \array_map(
                static function (string $data): int {
                    // "mb_" is available if overload is used, so use it ...
                    return \mb_strlen($data, 'CP850'); // 8-BIT
                },
                self::str_split($str)
            );
        }

        return \array_map('\strlen', self::str_split($str));
    }

    /**
     * Get a decimal code representation of a specific character.
     *
     * INFO: opposite to UTF8::decimal_to_chr()
     *
     * EXAMPLE: <code>UTF8::chr_to_decimal('§'); // 0xa7</code>
     *
     * @param string $char <p>The input character.</p>
     *
     * @psalm-pure
     *
     * @return int
     */
    public static function chr_to_decimal(string $char): int
    {
        if (self::$SUPPORT['iconv'] === true) {
            $chr_tmp = \iconv('UTF-8', 'UCS-4LE', $char);
            if ($chr_tmp !== false) {
                /** @phpstan-ignore-next-line - "unpack": only false if the format string contains errors */
                return \unpack('V', $chr_tmp)[1];
            }
        }

        $code = self::ord($char[0]);
        $bytes = 1;

        if (!($code & 0x80)) {
            // 0xxxxxxx
            return $code;
        }

        if (($code & 0xe0) === 0xc0) {
            // 110xxxxx
            $bytes = 2;
            $code &= ~0xc0;
        } elseif (($code & 0xf0) === 0xe0) {
            // 1110xxxx
            $bytes = 3;
            $code &= ~0xe0;
        } elseif (($code & 0xf8) === 0xf0) {
            // 11110xxx
            $bytes = 4;
            $code &= ~0xf0;
        }

        for ($i = 2; $i <= $bytes; ++$i) {
            // 10xxxxxx
            $code = ($code << 6) + (self::ord($char[$i - 1]) & ~0x80);
        }

        return $code;
    }

    /**
     * Get hexadecimal code point (U+xxxx) of a UTF-8 encoded character.
     *
     * EXAMPLE: <code>UTF8::chr_to_hex('§'); // U+00a7</code>
     *
     * @param int|string $char   <p>The input character</p>
     * @param string     $prefix [optional]
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The code point encoded as U+xxxx.</p>
     */
    public static function chr_to_hex($char, string $prefix = 'U+'): string
    {
        if ($char === '') {
            return '';
        }

        if ($char === '&#0;') {
            $char = '';
        }

        return self::int_to_hex(self::ord((string) $char), $prefix);
    }

    /**
     * Splits a string into smaller chunks and multiple lines, using the specified line ending character.
     *
     * EXAMPLE: <code>UTF8::chunk_split('ABC-ÖÄÜ-中文空白-κόσμε', 3); // "ABC\r\n-ÖÄ\r\nÜ-中\r\n文空白\r\n-κό\r\nσμε"</code>
     *
     * @param string $body         <p>The original string to be split.</p>
     * @param int    $chunk_length [optional] <p>The maximum character length of a chunk.</p>
     * @param string $end          [optional] <p>The character(s) to be inserted at the end of each chunk.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The chunked string.</p>
     */
    public static function chunk_split(string $body, int $chunk_length = 76, string $end = "\r\n"): string
    {
        return \implode($end, self::str_split($body, $chunk_length));
    }

    /**
     * Accepts a string and removes all non-UTF-8 characters from it + extras if needed.
     *
     * EXAMPLE: <code>UTF8::clean("\xEF\xBB\xBF„Abcdef\xc2\xa0\x20…” — 😃 - Düsseldorf", true, true); // '„Abcdef  …” — 😃 - Düsseldorf'</code>
     *
     * @param string $str                                     <p>The string to be sanitized.</p>
     * @param bool   $remove_bom                              [optional] <p>Set to true, if you need to remove
     *                                                        UTF-BOM.</p>
     * @param bool   $normalize_whitespace                    [optional] <p>Set to true, if you need to normalize the
     *                                                        whitespace.</p>
     * @param bool   $normalize_msword                        [optional] <p>Set to true, if you need to normalize MS
     *                                                        Word chars e.g.: "…"
     *                                                        => "..."</p>
     * @param bool   $keep_non_breaking_space                 [optional] <p>Set to true, to keep non-breaking-spaces,
     *                                                        in
     *                                                        combination with
     *                                                        $normalize_whitespace</p>
     * @param bool   $replace_diamond_question_mark           [optional] <p>Set to true, if you need to remove diamond
     *                                                        question mark e.g.: "�"</p>
     * @param bool   $remove_invisible_characters             [optional] <p>Set to false, if you not want to remove
     *                                                        invisible characters e.g.: "\0"</p>
     * @param bool   $remove_invisible_characters_url_encoded [optional] <p>Set to true, if you not want to remove
     *                                                        invisible url encoded characters e.g.: "%0B"<br> WARNING:
     *                                                        maybe contains false-positives e.g. aa%0Baa -> aaaa.
     *                                                        </p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>An clean UTF-8 encoded string.</p>
     */
    public static function clean(
        string $str,
        bool $remove_bom = false,
        bool $normalize_whitespace = false,
        bool $normalize_msword = false,
        bool $keep_non_breaking_space = false,
        bool $replace_diamond_question_mark = false,
        bool $remove_invisible_characters = true,
        bool $remove_invisible_characters_url_encoded = false
    ): string {
        // http://stackoverflow.com/questions/1401317/remove-non-utf8-characters-from-string
        // caused connection reset problem on larger strings

        $regex = '/
          (
            (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
            |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
            |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
            |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3
            ){1,100}                      # ...one or more times
          )
        | ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
        | ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
        /x';
        $str = (string) \preg_replace($regex, '$1', $str);

        if ($replace_diamond_question_mark) {
            $str = self::replace_diamond_question_mark($str);
        }

        if ($remove_invisible_characters) {
            $str = self::remove_invisible_characters($str, $remove_invisible_characters_url_encoded);
        }

        if ($normalize_whitespace) {
            $str = self::normalize_whitespace($str, $keep_non_breaking_space);
        }

        if ($normalize_msword) {
            $str = self::normalize_msword($str);
        }

        if ($remove_bom) {
            $str = self::remove_bom($str);
        }

        return $str;
    }

    /**
     * Clean-up a string and show only printable UTF-8 chars at the end  + fix UTF-8 encoding.
     *
     * EXAMPLE: <code>UTF8::cleanup("\xEF\xBB\xBF„Abcdef\xc2\xa0\x20…” — 😃 - Düsseldorf", true, true); // '„Abcdef  …” — 😃 - Düsseldorf'</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function cleanup($str): string
    {
        // init
        $str = (string) $str;

        if ($str === '') {
            return '';
        }

        // fixed ISO <-> UTF-8 Errors
        $str = self::fix_simple_utf8($str);

        // remove all none UTF-8 symbols
        // && remove diamond question mark (�)
        // && remove remove invisible characters (e.g. "\0")
        // && remove BOM
        // && normalize whitespace chars (but keep non-breaking-spaces)
        return self::clean(
            $str,
            true,
            true,
            false,
            true,
            true
        );
    }

    /**
     * Accepts a string or a array of strings and returns an array of Unicode code points.
     *
     * INFO: opposite to UTF8::string()
     *
     * EXAMPLE: <code>
     * UTF8::codepoints('κöñ'); // array(954, 246, 241)
     * // ... OR ...
     * UTF8::codepoints('κöñ', true); // array('U+03ba', 'U+00f6', 'U+00f1')
     * </code>
     *
     * @param string|string[] $arg         <p>A UTF-8 encoded string or an array of such strings.</p>
     * @param bool            $use_u_style <p>If True, will return code points in U+xxxx format,
     *                                     default, code points will be returned as integers.</p>
     *
     * @psalm-pure
     *
     * @return int[]|string[]
     *                        <p>
     *                        The array of code points:<br>
     *                        int[] for $u_style === false<br>
     *                        string[] for $u_style === true<br>
     *                        </p>
     */
    public static function codepoints($arg, bool $use_u_style = false): array
    {
        if (\is_string($arg)) {
            $arg = self::str_split($arg);
        }

        /**
         * @psalm-suppress DocblockTypeContradiction
         */
        if (!\is_array($arg)) {
            return [];
        }

        if ($arg === []) {
            return [];
        }

        $arg = \array_map(
            [
                self::class,
                'ord',
            ],
            $arg
        );

        if ($use_u_style) {
            $arg = \array_map(
                [
                    self::class,
                    'int_to_hex',
                ],
                $arg
            );
        }

        return $arg;
    }

    /**
     * Trims the string and replaces consecutive whitespace characters with a
     * single space. This includes tabs and newline characters, as well as
     * multibyte whitespace such as the thin space and ideographic space.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with trimmed $str and condensed whitespace.</p>
     */
    public static function collapse_whitespace(string $str): string
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \trim((string) \mb_ereg_replace('[[:space:]]+', ' ', $str));
        }

        return \trim(self::regex_replace($str, '[[:space:]]+', ' '));
    }

    /**
     * Returns count of characters used in a string.
     *
     * EXAMPLE: <code>UTF8::count_chars('κaκbκc'); // array('κ' => 3, 'a' => 1, 'b' => 1, 'c' => 1)</code>
     *
     * @param string $str                     <p>The input string.</p>
     * @param bool   $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param bool   $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
     *
     * @psalm-pure
     *
     * @return int[]
     *               <p>An associative array of Character as keys and
     *               their count as values.</p>
     */
    public static function count_chars(
        string $str,
        bool $clean_utf8 = false,
        bool $try_to_use_mb_functions = true
    ): array {
        return \array_count_values(
            self::str_split(
                $str,
                1,
                $clean_utf8,
                $try_to_use_mb_functions
            )
        );
    }

    /**
     * Create a valid CSS identifier for e.g. "class"- or "id"-attributes.
     *
     * EXAMPLE: <code>UTF8::css_identifier('123foo/bar!!!'); // _23foo-bar</code>
     *
     * copy&past from https://github.com/drupal/core/blob/8.8.x/lib/Drupal/Component/Utility/Html.php#L95
     *
     * @param string   $str        <p>INFO: if no identifier is given e.g. " " or "", we will create a unique string automatically</p>
     * @param string[] $filter
     * @param bool     $strip_tags
     * @param bool     $strtolower
     *
     * @psalm-pure
     *
     * @return string
     *
     * @phpstan-param array<string,string> $filter
     */
    public static function css_identifier(
        string $str = '',
        array $filter = [
            ' ' => '-',
            '/' => '-',
            '[' => '',
            ']' => '',
        ],
        bool $strip_tags = false,
        bool $strtolower = true
    ): string {
        // We could also use strtr() here but its much slower than str_replace(). In
        // order to keep '__' to stay '__' we first replace it with a different
        // placeholder after checking that it is not defined as a filter.
        $double_underscore_replacements = 0;

        // Fallback ...
        if (\trim($str) === '') {
            $str = \uniqid('auto-generated-css-class', true);
        } else {
            $str = self::clean($str);
        }

        if ($strip_tags) {
            $str = \strip_tags($str);
        }

        if ($strtolower) {
            $str = \strtolower($str);
        }

        if (!isset($filter['__'])) {
            $str = \str_replace('__', '##', $str, $double_underscore_replacements);
        }

        $str = \str_replace(\array_keys($filter), \array_values($filter), $str);
        // Replace temporary placeholder '##' with '__' only if the original
        // $identifier contained '__'.
        if ($double_underscore_replacements > 0) {
            $str = \str_replace('##', '__', $str);
        }

        // Valid characters in a CSS identifier are:
        // - the hyphen (U+002D)
        // - a-z (U+0030 - U+0039)
        // - A-Z (U+0041 - U+005A)
        // - the underscore (U+005F)
        // - 0-9 (U+0061 - U+007A)
        // - ISO 10646 characters U+00A1 and higher
        // We strip out any character not in the above list.
        $str = (string) \preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $str);
        // Identifiers cannot start with a digit, two hyphens, or a hyphen followed by a digit.
        $str = (string) \preg_replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__'], $str);

        return \trim($str, '-');
    }

    /**
     * Remove css media-queries.
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function css_stripe_media_queries(string $str): string
    {
        return (string) \preg_replace(
            '#@media\\s+(?:only\\s)?(?:[\\s{(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#isumU',
            '',
            $str
        );
    }

    /**
     * Checks whether ctype is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function ctype_loaded(): bool
    {
        return \extension_loaded('ctype');
    }

    /**
     * Converts an int value into a UTF-8 character.
     *
     * INFO: opposite to UTF8::string()
     *
     * EXAMPLE: <code>UTF8::decimal_to_chr(931); // 'Σ'</code>
     *
     * @param int|string $int
     *
     * @phpstan-param int|numeric-string $int
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function decimal_to_chr($int): string
    {
        // We cannot use html_entity_decode() here, as it will not return
        // characters for many values < 160.
        return mb_convert_encoding('&#' . $int . ';', 'UTF-8', 'HTML-ENTITIES');
    }

    /**
     * Decodes a MIME header field
     *
     * @param string $str
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>A decoded MIME field on success,
     *                      or false if an error occurs during the decoding.</p>
     */
    public static function decode_mimeheader($str, string $encoding = 'UTF-8')
    {
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        // always fallback via symfony polyfill
        return \iconv_mime_decode($str, \ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding);
    }

    /**
     * Convert any two-letter country code (ISO 3166-1) to the corresponding Emoji.
     *
     * @see https://en.wikipedia.org/wiki/ISO_3166-1
     *
     * @param string $country_code_iso_3166_1 <p>e.g. DE</p>
     *
     * @return string
     *                <p>Emoji or empty string on error.</p>
     */
    public static function emoji_from_country_code(string $country_code_iso_3166_1): string
    {
        if ($country_code_iso_3166_1 === '') {
            return '';
        }

        if (self::strlen($country_code_iso_3166_1) !== 2) {
            return '';
        }

        $country_code_iso_3166_1 = \strtoupper($country_code_iso_3166_1);

        $flagOffset = 0x1F1E6;
        $asciiOffset = 0x41;

        return (self::chr((self::ord($country_code_iso_3166_1[0]) - $asciiOffset + $flagOffset)) ?? '') .
               (self::chr((self::ord($country_code_iso_3166_1[1]) - $asciiOffset + $flagOffset)) ?? '');
    }

    /**
     * Decodes a string which was encoded by "UTF8::emoji_encode()".
     *
     * INFO: opposite to UTF8::emoji_encode()
     *
     * EXAMPLE: <code>
     * UTF8::emoji_decode('foo CHARACTER_OGRE', false); // 'foo 👹'
     * //
     * UTF8::emoji_decode('foo _-_PORTABLE_UTF8_-_308095726_-_627590803_-_8FTU_ELBATROP_-_', true); // 'foo 👹'
     * </code>
     *
     * @param string $str                            <p>The input string.</p>
     * @param bool   $use_reversible_string_mappings [optional] <p>
     *                                               When <b>TRUE</b>, we se a reversible string mapping
     *                                               between "emoji_encode" and "emoji_decode".</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function emoji_decode(
        string $str,
        bool $use_reversible_string_mappings = false
    ): string {
        if (self::$EMOJI_KEYS_CACHE === null) {
            /** @phpstan-ignore-next-line - we need to load the data first */
            self::initEmojiData();
        }

        if ($use_reversible_string_mappings) {
            return (string) \str_replace(
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
                (array) self::$EMOJI_VALUES_CACHE,
                $str
            );
        }

        return (string) \str_replace(
            (array) self::$EMOJI_KEYS_CACHE,
            (array) self::$EMOJI_VALUES_CACHE,
            $str
        );
    }

    /**
     * Encode a string with emoji chars into a non-emoji string.
     *
     * INFO: opposite to UTF8::emoji_decode()
     *
     * EXAMPLE: <code>
     * UTF8::emoji_encode('foo 👹', false)); // 'foo CHARACTER_OGRE'
     * //
     * UTF8::emoji_encode('foo 👹', true)); // 'foo _-_PORTABLE_UTF8_-_308095726_-_627590803_-_8FTU_ELBATROP_-_'
     * </code>
     *
     * @param string $str                            <p>The input string</p>
     * @param bool   $use_reversible_string_mappings [optional] <p>
     *                                               when <b>TRUE</b>, we use a reversible string mapping
     *                                               between "emoji_encode" and "emoji_decode"</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function emoji_encode(
        string $str,
        bool $use_reversible_string_mappings = false
    ): string {
        if (self::$EMOJI_KEYS_CACHE === null) {
            /** @phpstan-ignore-next-line - we need to load the data first */
            self::initEmojiData();
        }

        if ($use_reversible_string_mappings) {
            return (string) \str_replace(
                (array) self::$EMOJI_VALUES_CACHE,
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
                $str
            );
        }

        return (string) \str_replace(
            (array) self::$EMOJI_VALUES_CACHE,
            (array) self::$EMOJI_KEYS_CACHE,
            $str
        );
    }

    /**
     * Encode a string with a new charset-encoding.
     *
     * INFO:  This function will also try to fix broken / double encoding,
     *        so you can call this function also on a UTF-8 string and you don't mess up the string.
     *
     * EXAMPLE: <code>
     * UTF8::encode('ISO-8859-1', '-ABC-中文空白-'); // '-ABC-????-'
     * //
     * UTF8::encode('UTF-8', '-ABC-中文空白-'); // '-ABC-中文空白-'
     * //
     * UTF8::encode('HTML', '-ABC-中文空白-'); // '-ABC-&#20013;&#25991;&#31354;&#30333;-'
     * //
     * UTF8::encode('BASE64', '-ABC-中文空白-'); // 'LUFCQy3kuK3mlofnqbrnmb0t'
     * </code>
     *
     * @param string $to_encoding                   <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.</p>
     * @param string $str                           <p>The input string</p>
     * @param bool   $auto_detect_the_from_encoding [optional] <p>Force the new encoding (we try to fix broken / double
     *                                              encoding for UTF-8)<br> otherwise we auto-detect the current
     *                                              string-encoding</p>
     * @param string $from_encoding                 [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
     *                                              A empty string will trigger the autodetect anyway.</p>
     *
     * @psalm-pure
     *
     * @return string
     *
     * @psalm-suppress InvalidReturnStatement
     */
    public static function encode(
        string $to_encoding,
        string $str,
        bool $auto_detect_the_from_encoding = true,
        string $from_encoding = ''
    ): string {
        if ($str === '' || $to_encoding === '') {
            return $str;
        }

        if ($to_encoding !== 'UTF-8' && $to_encoding !== 'CP850') {
            $to_encoding = self::normalize_encoding($to_encoding, 'UTF-8');
        }

        if ($from_encoding && $from_encoding !== 'UTF-8' && $from_encoding !== 'CP850') {
            $from_encoding = self::normalize_encoding($from_encoding);
        }

        if (
            $to_encoding
            &&
            $from_encoding
            &&
            $from_encoding === $to_encoding
        ) {
            return $str;
        }

        if ($to_encoding === 'JSON') {
            $return = self::json_encode($str);
            if ($return === false) {
                throw new \InvalidArgumentException('The input string [' . $str . '] can not be used for json_encode().');
            }

            return $return;
        }
        if ($from_encoding === 'JSON') {
            $str = self::json_decode($str);
            $from_encoding = '';
        }

        if ($to_encoding === 'BASE64') {
            return \base64_encode($str);
        }
        if ($from_encoding === 'BASE64') {
            $str = \base64_decode($str, true);
            $from_encoding = '';
        }

        if ($to_encoding === 'HTML-ENTITIES') {
            return self::html_encode($str, true);
        }
        if ($from_encoding === 'HTML-ENTITIES') {
            $str = self::html_entity_decode($str, \ENT_COMPAT);
            $from_encoding = '';
        }

        $from_encoding_auto_detected = false;
        if (
            $auto_detect_the_from_encoding
            ||
            !$from_encoding
        ) {
            $from_encoding_auto_detected = self::str_detect_encoding($str);
        }

        // DEBUG
        //var_dump($to_encoding, $from_encoding, $from_encoding_auto_detected, $str, "\n\n");

        if ($from_encoding_auto_detected !== false) {
            $from_encoding = $from_encoding_auto_detected;
        } elseif ($auto_detect_the_from_encoding) {
            // fallback for the "autodetect"-mode
            return self::to_utf8($str);
        }

        if (
            !$from_encoding
            ||
            $from_encoding === $to_encoding
        ) {
            return $str;
        }

        if (
            $to_encoding === 'UTF-8'
            &&
            (
                $from_encoding === 'WINDOWS-1252'
                ||
                $from_encoding === 'ISO-8859-1'
            )
        ) {
            return self::to_utf8($str);
        }

        if (
            $to_encoding === 'ISO-8859-1'
            &&
            (
                $from_encoding === 'WINDOWS-1252'
                ||
                $from_encoding === 'UTF-8'
            )
        ) {
            return self::to_iso8859($str);
        }

        if (
            $to_encoding !== 'UTF-8'
            &&
            $to_encoding !== 'ISO-8859-1'
            &&
            $to_encoding !== 'WINDOWS-1252'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::encode() without mbstring cannot handle "' . $to_encoding . '" encoding', \E_USER_WARNING);
        }

        if (self::$SUPPORT['mbstring'] === true) {
            $str_encoded = \mb_convert_encoding(
                $str,
                $to_encoding,
                $from_encoding
            );

            if ($str_encoded) {
                \assert(\is_string($str_encoded));

                return $str_encoded;
            }
        }

        /** @noinspection PhpUsageOfSilenceOperatorInspection - Detected an incomplete multibyte character in input string */
        $return = @\iconv($from_encoding, $to_encoding, $str);
        if ($return !== false) {
            return $return;
        }

        return $str;
    }

    /**
     * @param string $str
     * @param string $from_charset      [optional] <p>Set the input charset.</p>
     * @param string $to_charset        [optional] <p>Set the output charset.</p>
     * @param string $transfer_encoding [optional] <p>Set the transfer encoding.</p>
     * @param string $linefeed          [optional] <p>Set the used linefeed.</p>
     * @param int    $indent            [optional] <p>Set the max length indent.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>An encoded MIME field on success,
     *                      or false if an error occurs during the encoding.</p>
     */
    public static function encode_mimeheader(
        string $str,
        string $from_charset = 'UTF-8',
        string $to_charset = 'UTF-8',
        string $transfer_encoding = 'Q',
        string $linefeed = "\r\n",
        int $indent = 76
    ) {
        if ($from_charset !== 'UTF-8' && $from_charset !== 'CP850') {
            $from_charset = self::normalize_encoding($from_charset, 'UTF-8');
        }

        if ($to_charset !== 'UTF-8' && $to_charset !== 'CP850') {
            $to_charset = self::normalize_encoding($to_charset, 'UTF-8');
        }

        // always fallback via symfony polyfill
        return \iconv_mime_encode(
            '',
            $str,
            [
                'scheme'           => $transfer_encoding,
                'line-length'      => $indent,
                'input-charset'    => $from_charset,
                'output-charset'   => $to_charset,
                'line-break-chars' => $linefeed,
            ]
        );
    }

    /**
     * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
     *
     * @param string   $str                       <p>The input string.</p>
     * @param string   $search                    <p>The searched string.</p>
     * @param int|null $length                    [optional] <p>Default: null === text->length / 2</p>
     * @param string   $replacer_for_skipped_text [optional] <p>Default: …</p>
     * @param string   $encoding                  [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function extract_text(
        string $str,
        string $search = '',
        int $length = null,
        string $replacer_for_skipped_text = '…',
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '') {
            return '';
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        $trim_chars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";

        if ($length === null) {
            $length = (int) \round((int) self::strlen($str, $encoding) / 2);
        }

        if ($search === '') {
            if ($encoding === 'UTF-8') {
                if ($length > 0) {
                    $string_length = (int) \mb_strlen($str);
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
                } else {
                    $end = 0;
                }

                $pos = (int) \min(
                    \mb_strpos($str, ' ', $end),
                    \mb_strpos($str, '.', $end)
                );
            } else {
                if ($length > 0) {
                    $string_length = (int) self::strlen($str, $encoding);
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
                } else {
                    $end = 0;
                }

                $pos = (int) \min(
                    self::strpos($str, ' ', $end, $encoding),
                    self::strpos($str, '.', $end, $encoding)
                );
            }

            if ($pos) {
                if ($encoding === 'UTF-8') {
                    $str_sub = \mb_substr($str, 0, $pos);
                } else {
                    $str_sub = self::substr($str, 0, $pos, $encoding);
                }

                if ($str_sub === false) {
                    return '';
                }

                return \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
            }

            return $str;
        }

        if ($encoding === 'UTF-8') {
            $word_position = (int) \mb_stripos($str, $search);
            $half_side = (int) ($word_position - $length / 2 + (int) \mb_strlen($search) / 2);
        } else {
            $word_position = (int) self::stripos($str, $search, 0, $encoding);
            $half_side = (int) ($word_position - $length / 2 + (int) self::strlen($search, $encoding) / 2);
        }

        $pos_start = 0;
        if ($half_side > 0) {
            if ($encoding === 'UTF-8') {
                $half_text = \mb_substr($str, 0, $half_side);
            } else {
                $half_text = self::substr($str, 0, $half_side, $encoding);
            }
            if ($half_text !== false) {
                if ($encoding === 'UTF-8') {
                    $pos_start = (int) \max(
                        \mb_strrpos($half_text, ' '),
                        \mb_strrpos($half_text, '.')
                    );
                } else {
                    $pos_start = (int) \max(
                        self::strrpos($half_text, ' ', 0, $encoding),
                        self::strrpos($half_text, '.', 0, $encoding)
                    );
                }
            }
        }

        if ($word_position && $half_side > 0) {
            $offset = $pos_start + $length - 1;
            $real_length = (int) self::strlen($str, $encoding);

            if ($offset > $real_length) {
                $offset = $real_length;
            }

            if ($encoding === 'UTF-8') {
                $pos_end = (int) \min(
                    \mb_strpos($str, ' ', $offset),
                    \mb_strpos($str, '.', $offset)
                ) - $pos_start;
            } else {
                $pos_end = (int) \min(
                    self::strpos($str, ' ', $offset, $encoding),
                    self::strpos($str, '.', $offset, $encoding)
                ) - $pos_start;
            }

            if (!$pos_end || $pos_end <= 0) {
                if ($encoding === 'UTF-8') {
                    $str_sub = \mb_substr($str, $pos_start, (int) \mb_strlen($str));
                } else {
                    $str_sub = self::substr($str, $pos_start, (int) self::strlen($str, $encoding), $encoding);
                }
                if ($str_sub !== false) {
                    $extract = $replacer_for_skipped_text . \ltrim($str_sub, $trim_chars);
                } else {
                    $extract = '';
                }
            } else {
                if ($encoding === 'UTF-8') {
                    $str_sub = \mb_substr($str, $pos_start, $pos_end);
                } else {
                    $str_sub = self::substr($str, $pos_start, $pos_end, $encoding);
                }
                if ($str_sub !== false) {
                    $extract = $replacer_for_skipped_text . \trim($str_sub, $trim_chars) . $replacer_for_skipped_text;
                } else {
                    $extract = '';
                }
            }
        } else {
            $offset = $length - 1;
            $true_length = (int) self::strlen($str, $encoding);

            if ($offset > $true_length) {
                $offset = $true_length;
            }

            if ($encoding === 'UTF-8') {
                $pos_end = (int) \min(
                    \mb_strpos($str, ' ', $offset),
                    \mb_strpos($str, '.', $offset)
                );
            } else {
                $pos_end = (int) \min(
                    self::strpos($str, ' ', $offset, $encoding),
                    self::strpos($str, '.', $offset, $encoding)
                );
            }

            if ($pos_end) {
                if ($encoding === 'UTF-8') {
                    $str_sub = \mb_substr($str, 0, $pos_end);
                } else {
                    $str_sub = self::substr($str, 0, $pos_end, $encoding);
                }
                if ($str_sub !== false) {
                    $extract = \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
                } else {
                    $extract = '';
                }
            } else {
                $extract = $str;
            }
        }

        return $extract;
    }

    /**
     * Reads entire file into a string.
     *
     * EXAMPLE: <code>UTF8::file_get_contents('utf16le.txt'); // ...</code>
     *
     * WARNING: Do not use UTF-8 Option ($convert_to_utf8) for binary files (e.g.: images) !!!
     *
     * @see http://php.net/manual/en/function.file-get-contents.php
     *
     * @param string        $filename         <p>
     *                                        Name of the file to read.
     *                                        </p>
     * @param bool          $use_include_path [optional] <p>
     *                                        Prior to PHP 5, this parameter is called
     *                                        use_include_path and is a bool.
     *                                        As of PHP 5 the FILE_USE_INCLUDE_PATH can be used
     *                                        to trigger include path
     *                                        search.
     *                                        </p>
     * @param resource|null $context          [optional] <p>
     *                                        A valid context resource created with
     *                                        stream_context_create. If you don't need to use a
     *                                        custom context, you can skip this parameter by &null;.
     *                                        </p>
     * @param int|null      $offset           [optional] <p>
     *                                        The offset where the reading starts.
     *                                        </p>
     * @param int|null      $max_length       [optional] <p>
     *                                        Maximum length of data read. The default is to read until end
     *                                        of file is reached.
     *                                        </p>
     * @param int           $timeout          <p>The time in seconds for the timeout.</p>
     * @param bool          $convert_to_utf8  <strong>WARNING!!!</strong> <p>Maybe you can't use this option for
     *                                        some files, because they used non default utf-8 chars. Binary files
     *                                        like images or pdf will not be converted.</p>
     * @param string        $from_encoding    [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
     *                                        A empty string will trigger the autodetect anyway.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>The function returns the read data as string or <b>false</b> on failure.</p>
     */
    public static function file_get_contents(
        string $filename,
        bool $use_include_path = false,
        $context = null,
        int $offset = null,
        int $max_length = null,
        int $timeout = 10,
        bool $convert_to_utf8 = true,
        string $from_encoding = ''
    ) {
        // init
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - is ok here */
        $filename = Bootup::filter_sanitize_string_polyfill($filename);
        if ($filename === false) {
            return false;
        }

        if ($timeout && $context === null) {
            $context = \stream_context_create(
                [
                    'http' => [
                        'timeout' => $timeout,
                    ],
                ]
            );
        }

        if ($offset === null) {
            $offset = 0;
        }

        if (\is_int($max_length)) {

            if ($max_length < 0) {
                $max_length = 0;
            }

            $data = \file_get_contents($filename, $use_include_path, $context, $offset, $max_length);
        } else {
            $data = \file_get_contents($filename, $use_include_path, $context, $offset);
        }

        // return false on error
        if ($data === false) {
            return false;
        }

        if ($convert_to_utf8) {
            if (
                !self::is_binary($data, true)
                ||
                self::is_utf16($data, false) !== false
                ||
                self::is_utf32($data, false) !== false
            ) {
                $data = self::encode('UTF-8', $data, false, $from_encoding);
                $data = self::cleanup($data);
            }
        }

        return $data;
    }

    /**
     * Checks if a file starts with BOM (Byte Order Mark) character.
     *
     * EXAMPLE: <code>UTF8::file_has_bom('utf8_with_bom.txt'); // true</code>
     *
     * @param string $file_path <p>Path to a valid file.</p>
     *
     * @throws \RuntimeException if file_get_contents() returned false
     *
     * @return bool
     *              <p><strong>true</strong> if the file has BOM at the start, <strong>false</strong> otherwise</p>
     *
     * @psalm-pure
     */
    public static function file_has_bom(string $file_path): bool
    {
        $file_content = \file_get_contents($file_path);
        if ($file_content === false) {
            throw new \RuntimeException('file_get_contents() returned false for:' . $file_path);
        }

        return self::string_has_bom($file_content);
    }

    /**
     * Normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
     *
     * EXAMPLE: <code>UTF8::filter(array("\xE9", 'à', 'a')); // array('é', 'à', 'a')</code>
     *
     * @param array|object|string $var
     * @param int                 $normalization_form
     * @param string              $leading_combining
     *
     * @psalm-pure
     *
     * @return mixed
     *
     * @template TFilter
     * @phpstan-param TFilter $var
     * @phpstan-return TFilter
     */
    public static function filter(
        $var,
        int $normalization_form = \Normalizer::NFC,
        string $leading_combining = '◌'
    ) {
        switch (\gettype($var)) {
            case 'object':
            case 'array':
                foreach ($var as &$v) {
                    $v = self::filter($v, $normalization_form, $leading_combining);
                }
                unset($v);

                break;
            case 'string':

                if (\strpos($var, "\r") !== false) {
                    $var = self::normalize_line_ending($var);
                }

                if (!ASCII::is_ascii($var)) {
                    if (\Normalizer::isNormalized($var, $normalization_form)) {
                        $n = '-';
                    } else {
                        $n = \Normalizer::normalize($var, $normalization_form);

                        if ($n && isset($n[0])) {
                            $var = $n;
                        } else {
                            $var = self::encode('UTF-8', $var);
                        }
                    }

                    \assert(\is_string($var));
                    if (
                        $n
                        &&
                        $var[0] >= "\x80"
                        &&
                        isset($n[0], $leading_combining[0])
                        &&
                        \preg_match('/^\\p{Mn}/u', $var)
                    ) {
                        // Prevent leading combining chars
                        // for NFC-safe concatenations.
                        $var = $leading_combining . $var;
                    }
                }

                break;
            default:
                // nothing
        }

        /** @noinspection PhpSillyAssignmentInspection */
        /** @phpstan-var TFilter $var */
        $var = $var;

        return $var;
    }

    /**
     * "filter_input()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
     *
     * Gets a specific external variable by name and optionally filters it.
     *
     * EXAMPLE: <code>
     * // _GET['foo'] = 'bar';
     * UTF8::filter_input(INPUT_GET, 'foo', FILTER_UNSAFE_RAW)); // 'bar'
     * </code>
     *
     * @see http://php.net/manual/en/function.filter-input.php
     *
     * @param int            $type          <p>
     *                                      One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
     *                                      <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
     *                                      <b>INPUT_ENV</b>.
     *                                      </p>
     * @param string         $variable_name <p>
     *                                      Name of a variable to get.
     *                                      </p>
     * @param int            $filter        [optional] <p>
     *                                      The ID of the filter to apply. The
     *                                      manual page lists the available filters.
     *                                      </p>
     * @param int|int[]|null $options       [optional] <p>
     *                                      Associative array of options or bitwise disjunction of flags. If filter
     *                                      accepts options, flags can be provided in "flags" field of array.
     *                                      </p>
     *
     * @psalm-pure
     *
     * @return mixed
     *               <p>
     *               Value of the requested variable on success, <b>FALSE</b> if the filter fails, or <b>NULL</b> if the
     *               <i>variable_name</i> variable is not set. If the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it
     *               returns <b>FALSE</b> if the variable is not set and <b>NULL</b> if the filter fails.
     *               </p>
     */
    public static function filter_input(
        int $type,
        string $variable_name,
        int $filter = \FILTER_DEFAULT,
        $options = null
    ) {
        /**
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
         */
        if ($options === null || \func_num_args() < 4) {
            $var = \filter_input($type, $variable_name, $filter);
        } else {
            $var = \filter_input($type, $variable_name, $filter, $options);
        }

        return self::filter($var);
    }

    /**
     * "filter_input_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
     *
     * Gets external variables and optionally filters them.
     *
     * EXAMPLE: <code>
     * // _GET['foo'] = 'bar';
     * UTF8::filter_input_array(INPUT_GET, array('foo' => 'FILTER_UNSAFE_RAW')); // array('bar')
     * </code>
     *
     * @see http://php.net/manual/en/function.filter-input-array.php
     *
     * @param int        $type       <p>
     *                               One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
     *                               <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
     *                               <b>INPUT_ENV</b>.
     *                               </p>
     * @param array|null $definition [optional] <p>
     *                               An array defining the arguments. A valid key is a string
     *                               containing a variable name and a valid value is either a filter type, or an array
     *                               optionally specifying the filter, flags and options. If the value is an
     *                               array, valid keys are filter which specifies the
     *                               filter type,
     *                               flags which specifies any flags that apply to the
     *                               filter, and options which specifies any options that
     *                               apply to the filter. See the example below for a better understanding.
     *                               </p>
     *                               <p>
     *                               This parameter can be also an integer holding a filter constant. Then all values in the
     *                               input array are filtered by this filter.
     *                               </p>
     * @param bool       $add_empty  [optional] <p>
     *                               Add missing keys as <b>NULL</b> to the return value.
     *                               </p>
     *
     * @psalm-pure
     *
     * @return mixed
     *               <p>
     *               An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
     *               set. Or if the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it returns <b>FALSE</b> if the variable
     *               is not set and <b>NULL</b> if the filter fails.
     *               </p>
     */
    public static function filter_input_array(
        int $type,
        $definition = null,
        bool $add_empty = true
    ) {
        /**
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
         */
        if ($definition === null || \func_num_args() < 2) {
            $a = \filter_input_array($type);
        } else {
            $a = \filter_input_array($type, $definition, $add_empty);
        }

        return self::filter($a);
    }

    /**
     * "filter_var()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
     *
     * Filters a variable with a specified filter.
     *
     * EXAMPLE: <code>UTF8::filter_var('-ABC-中文空白-', FILTER_VALIDATE_URL); // false</code>
     *
     * @see http://php.net/manual/en/function.filter-var.php
     *
     * @param float|int|string|null $variable <p>
     *                                        Value to filter.
     *                                        </p>
     * @param int                   $filter   [optional] <p>
     *                                        The ID of the filter to apply. The
     *                                        manual page lists the available filters.
     *                                        </p>
     * @param int|int[]|null        $options  [optional] <p>
     *                                        Associative array of options or bitwise disjunction of flags. If filter
     *                                        accepts options, flags can be provided in "flags" field of array. For
     *                                        the "callback" filter, callable type should be passed. The
     *                                        callback must accept one argument, the value to be filtered, and return
     *                                        the value after filtering/sanitizing it.
     *                                        </p>
     *                                        <p>
     *                                        <code>
     *                                        // for filters that accept options, use this format
     *                                        $options = array(
     *                                        'options' => array(
     *                                        'default' => 3, // value to return if the filter fails
     *                                        // other options here
     *                                        'min_range' => 0
     *                                        ),
     *                                        'flags' => FILTER_FLAG_ALLOW_OCTAL,
     *                                        );
     *                                        $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
     *                                        // for filter that only accept flags, you can pass them directly
     *                                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
     *                                        // for filter that only accept flags, you can also pass as an array
     *                                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN,
     *                                        array('flags' => FILTER_NULL_ON_FAILURE));
     *                                        // callback validate filter
     *                                        function foo($value)
     *                                        {
     *                                        // Expected format: Surname, GivenNames
     *                                        if (strpos($value, ", ") === false) return false;
     *                                        list($surname, $givennames) = explode(", ", $value, 2);
     *                                        $empty = (empty($surname) || empty($givennames));
     *                                        $notstrings = (!is_string($surname) || !is_string($givennames));
     *                                        if ($empty || $notstrings) {
     *                                        return false;
     *                                        } else {
     *                                        return $value;
     *                                        }
     *                                        }
     *                                        $var = filter_var('Doe, Jane Sue', FILTER_CALLBACK, array('options' => 'foo'));
     *                                        </code>
     *                                        </p>
     *
     * @psalm-pure
     *
     * @return mixed
     *               <p>The filtered data, or <b>FALSE</b> if the filter fails.</p>
     */
    public static function filter_var(
        $variable,
        int $filter = \FILTER_DEFAULT,
        $options = null
    ) {
        /**
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
         */
        if (\func_num_args() < 3) {
            $variable = \filter_var($variable, $filter);
        } else {
            $variable = \filter_var($variable, $filter, $options);
        }

        return self::filter($variable);
    }

    /**
     * "filter_var_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
     *
     * Gets multiple variables and optionally filters them.
     *
     * EXAMPLE: <code>
     * $filters = [
     *     'name'  => ['filter'  => FILTER_CALLBACK, 'options' => [UTF8::class, 'ucwords']],
     *     'age'   => ['filter'  => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 120]],
     *     'email' => FILTER_VALIDATE_EMAIL,
     * ];
     *
     * $data = [
     *     'name' => 'κόσμε',
     *     'age' => '18',
     *     'email' => 'foo@bar.de'
     * ];
     *
     * UTF8::filter_var_array($data, $filters, true); // ['name' => 'Κόσμε', 'age' => 18, 'email' => 'foo@bar.de']
     * </code>
     *
     * @see http://php.net/manual/en/function.filter-var-array.php
     *
     * @param array<mixed>   $data       <p>
     *                                   An array with string keys containing the data to filter.
     *                                   </p>
     * @param array|int|null $definition [optional] <p>
     *                                   An array defining the arguments. A valid key is a string
     *                                   containing a variable name and a valid value is either a
     *                                   filter type, or an
     *                                   array optionally specifying the filter, flags and options.
     *                                   If the value is an array, valid keys are filter
     *                                   which specifies the filter type,
     *                                   flags which specifies any flags that apply to the
     *                                   filter, and options which specifies any options that
     *                                   apply to the filter. See the example below for a better understanding.
     *                                   </p>
     *                                   <p>
     *                                   This parameter can be also an integer holding a filter constant. Then all values
     *                                   in the input array are filtered by this filter.
     *                                   </p>
     * @param bool           $add_empty  [optional] <p>
     *                                   Add missing keys as <b>NULL</b> to the return value.
     *                                   </p>
     *
     * @psalm-pure
     *
     * @return mixed
     *               <p>
     *               An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
     *               set.
     *               </p>
     */
    public static function filter_var_array(
        array $data,
        $definition = null,
        bool $add_empty = true
    ) {
        /**
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
         */
        if (\func_num_args() < 2) {
            $a = \filter_var_array($data);
        } else {
            $a = \filter_var_array($data, $definition, $add_empty);
        }

        return self::filter($a);
    }

    /**
     * Checks whether finfo is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function finfo_loaded(): bool
    {
        return \class_exists('finfo');
    }

    /**
     * Returns the first $n characters of the string.
     *
     * @param string $str      <p>The input string.</p>
     * @param int    $n        <p>Number of characters to retrieve from the start.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function first_char(
        string $str,
        int $n = 1,
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '' || $n <= 0) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr($str, 0, $n);
        }

        return (string) self::substr($str, 0, $n, $encoding);
    }

    /**
     * Check if the number of Unicode characters isn't greater than the specified integer.
     *
     * EXAMPLE: <code>UTF8::fits_inside('κόσμε', 6); // false</code>
     *
     * @param string $str      the original string to be checked
     * @param int    $box_size the size in number of chars to be checked against string
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>TRUE</strong> if string is less than or equal to $box_size, <strong>FALSE</strong> otherwise.</p>
     */
    public static function fits_inside(string $str, int $box_size): bool
    {
        return (int) self::strlen($str) <= $box_size;
    }

    /**
     * Try to fix simple broken UTF-8 strings.
     *
     * INFO: Take a look at "UTF8::fix_utf8()" if you need a more advanced fix for broken UTF-8 strings.
     *
     * EXAMPLE: <code>UTF8::fix_simple_utf8('Düsseldorf'); // 'Düsseldorf'</code>
     *
     * If you received an UTF-8 string that was converted from Windows-1252 as it was ISO-8859-1
     * (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it.
     * See: http://en.wikipedia.org/wiki/Windows-1252
     *
     * @param string $str <p>The input string</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function fix_simple_utf8(string $str): string
    {
        if ($str === '') {
            return '';
        }

        /**
         * @psalm-suppress ImpureStaticVariable
         *
         * @var array<mixed>|null
         */
        static $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = null;

        /**
         * @psalm-suppress ImpureStaticVariable
         *
         * @var array<mixed>|null
         */
        static $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = null;

        if ($BROKEN_UTF8_TO_UTF8_KEYS_CACHE === null) {
            if (self::$BROKEN_UTF8_FIX === null) {
                self::$BROKEN_UTF8_FIX = self::getData('utf8_fix');
            }

            $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = \array_keys(self::$BROKEN_UTF8_FIX ?: []);
            $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = self::$BROKEN_UTF8_FIX;
        }

        \assert(\is_array($BROKEN_UTF8_TO_UTF8_VALUES_CACHE));

        return \str_replace($BROKEN_UTF8_TO_UTF8_KEYS_CACHE, $BROKEN_UTF8_TO_UTF8_VALUES_CACHE, $str);
    }

    /**
     * Fix a double (or multiple) encoded UTF8 string.
     *
     * EXAMPLE: <code>UTF8::fix_utf8('Fédération'); // 'Fédération'</code>
     *
     * @param string|string[] $str you can use a string or an array of strings
     *
     * @psalm-pure
     *
     * @return string|string[]
     *                         <p>Will return the fixed input-"array" or
     *                         the fixed input-"string".</p>
     *
     * @template TFixUtf8
     * @phpstan-param TFixUtf8 $str
     * @phpstan-return TFixUtf8
     */
    public static function fix_utf8($str)
    {
        if (\is_array($str)) {
            foreach ($str as &$v) {
                $v = self::fix_utf8($v);
            }
            unset($v);

            /**
             * @psalm-suppress InvalidReturnStatement
             */
            return $str;
        }

        $str = (string) $str;
        $last = '';
        while ($last !== $str) {
            $last = $str;
            /**
             * @psalm-suppress PossiblyInvalidArgument
             */
            $str = self::to_utf8(
                self::utf8_decode($str, true)
            );
        }

        /**
         * @psalm-suppress InvalidReturnStatement
         */
        return $str;
    }

    /**
     * Get character of a specific character.
     *
     * EXAMPLE: <code>UTF8::getCharDirection('ا'); // 'RTL'</code>
     *
     * @param string $char
     *
     * @psalm-pure
     *
     * @return string
     *                <p>'RTL' or 'LTR'.</p>
     */
    public static function getCharDirection(string $char): string
    {
        if (self::$SUPPORT['intlChar'] === true) {
            $tmp_return = \IntlChar::charDirection($char);

            // from "IntlChar"-Class
            $char_direction = [
                'RTL' => [1, 13, 14, 15, 21],
                'LTR' => [0, 11, 12, 20],
            ];

            if (\in_array($tmp_return, $char_direction['LTR'], true)) {
                return 'LTR';
            }

            if (\in_array($tmp_return, $char_direction['RTL'], true)) {
                return 'RTL';
            }
        }

        $c = static::chr_to_decimal($char);

        if (!($c >= 0x5be && $c <= 0x10b7f)) {
            return 'LTR';
        }

        if ($c <= 0x85e) {
            if ($c === 0x5be ||
                $c === 0x5c0 ||
                $c === 0x5c3 ||
                $c === 0x5c6 ||
                ($c >= 0x5d0 && $c <= 0x5ea) ||
                ($c >= 0x5f0 && $c <= 0x5f4) ||
                $c === 0x608 ||
                $c === 0x60b ||
                $c === 0x60d ||
                $c === 0x61b ||
                ($c >= 0x61e && $c <= 0x64a) ||
                ($c >= 0x66d && $c <= 0x66f) ||
                ($c >= 0x671 && $c <= 0x6d5) ||
                ($c >= 0x6e5 && $c <= 0x6e6) ||
                ($c >= 0x6ee && $c <= 0x6ef) ||
                ($c >= 0x6fa && $c <= 0x70d) ||
                $c === 0x710 ||
                ($c >= 0x712 && $c <= 0x72f) ||
                ($c >= 0x74d && $c <= 0x7a5) ||
                $c === 0x7b1 ||
                ($c >= 0x7c0 && $c <= 0x7ea) ||
                ($c >= 0x7f4 && $c <= 0x7f5) ||
                $c === 0x7fa ||
                ($c >= 0x800 && $c <= 0x815) ||
                $c === 0x81a ||
                $c === 0x824 ||
                $c === 0x828 ||
                ($c >= 0x830 && $c <= 0x83e) ||
                ($c >= 0x840 && $c <= 0x858) ||
                $c === 0x85e
            ) {
                return 'RTL';
            }
        } elseif ($c === 0x200f) {
            return 'RTL';
        } elseif ($c >= 0xfb1d) {
            if ($c === 0xfb1d ||
                ($c >= 0xfb1f && $c <= 0xfb28) ||
                ($c >= 0xfb2a && $c <= 0xfb36) ||
                ($c >= 0xfb38 && $c <= 0xfb3c) ||
                $c === 0xfb3e ||
                ($c >= 0xfb40 && $c <= 0xfb41) ||
                ($c >= 0xfb43 && $c <= 0xfb44) ||
                ($c >= 0xfb46 && $c <= 0xfbc1) ||
                ($c >= 0xfbd3 && $c <= 0xfd3d) ||
                ($c >= 0xfd50 && $c <= 0xfd8f) ||
                ($c >= 0xfd92 && $c <= 0xfdc7) ||
                ($c >= 0xfdf0 && $c <= 0xfdfc) ||
                ($c >= 0xfe70 && $c <= 0xfe74) ||
                ($c >= 0xfe76 && $c <= 0xfefc) ||
                ($c >= 0x10800 && $c <= 0x10805) ||
                $c === 0x10808 ||
                ($c >= 0x1080a && $c <= 0x10835) ||
                ($c >= 0x10837 && $c <= 0x10838) ||
                $c === 0x1083c ||
                ($c >= 0x1083f && $c <= 0x10855) ||
                ($c >= 0x10857 && $c <= 0x1085f) ||
                ($c >= 0x10900 && $c <= 0x1091b) ||
                ($c >= 0x10920 && $c <= 0x10939) ||
                $c === 0x1093f ||
                $c === 0x10a00 ||
                ($c >= 0x10a10 && $c <= 0x10a13) ||
                ($c >= 0x10a15 && $c <= 0x10a17) ||
                ($c >= 0x10a19 && $c <= 0x10a33) ||
                ($c >= 0x10a40 && $c <= 0x10a47) ||
                ($c >= 0x10a50 && $c <= 0x10a58) ||
                ($c >= 0x10a60 && $c <= 0x10a7f) ||
                ($c >= 0x10b00 && $c <= 0x10b35) ||
                ($c >= 0x10b40 && $c <= 0x10b55) ||
                ($c >= 0x10b58 && $c <= 0x10b72) ||
                ($c >= 0x10b78)
            ) {
                return 'RTL';
            }
        }

        return 'LTR';
    }

    /**
     * Check for php-support.
     *
     * @param string|null $key
     *
     * @psalm-pure
     *
     * @return mixed
     *               Return the full support-"array", if $key === null<br>
     *               return bool-value, if $key is used and available<br>
     *               otherwise return <strong>null</strong>
     */
    public static function getSupportInfo(string $key = null)
    {
        if ($key === null) {
            return self::$SUPPORT;
        }

        if (self::$INTL_TRANSLITERATOR_LIST === null) {
            self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
        }
        // compatibility fix for old versions
        self::$SUPPORT['intl__transliterator_list_ids'] = self::$INTL_TRANSLITERATOR_LIST;

        return self::$SUPPORT[$key] ?? null;
    }

    /**
     * Warning: this method only works for some file-types (png, jpg)
     *          if you need more supported types, please use e.g. "finfo"
     *
     * @param string $str
     * @param array  $fallback <p>with this keys: 'ext', 'mime', 'type'
     *
     * @psalm-pure
     *
     * @return null[]|string[]
     *                         <p>with this keys: 'ext', 'mime', 'type'</p>
     *
     * @phpstan-param array{ext: null|string, mime: null|string, type: null|string} $fallback
     */
    public static function get_file_type(
        string $str,
        array $fallback = [
            'ext'  => null,
            'mime' => 'application/octet-stream',
            'type' => null,
        ]
    ): array {
        if ($str === '') {
            return $fallback;
        }

        /** @var false|string $str_info - needed for PhpStan (stubs error) */
        $str_info = \substr($str, 0, 2);
        if ($str_info === false || \strlen($str_info) !== 2) {
            return $fallback;
        }

        // DEBUG
        //var_dump($str_info);

        $str_info = \unpack('C2chars', $str_info);

        if ($str_info === false) {
            return $fallback;
        }
        $type_code = (int) ($str_info['chars1'] . $str_info['chars2']);

        // DEBUG
        //var_dump($type_code);

        //
        // info: https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Format_indicator
        //
        switch ($type_code) {
            // WARNING: do not add too simple comparisons, because of false-positive results:
            //
            // 3780 => 'pdf', 7790 => 'exe', 7784 => 'midi', 8075 => 'zip',
            // 8297 => 'rar', 7173 => 'gif', 7373 => 'tiff' 6677 => 'bmp', ...
            //
            case 255216:
                $ext = 'jpg';
                $mime = 'image/jpeg';
                $type = 'binary';

                break;
            case 13780:
                $ext = 'png';
                $mime = 'image/png';
                $type = 'binary';

                break;
            default:
                return $fallback;
        }

        return [
            'ext'  => $ext,
            'mime' => $mime,
            'type' => $type,
        ];
    }

    /**
     * @param int    $length         <p>Length of the random string.</p>
     * @param string $possible_chars [optional] <p>Characters string for the random selection.</p>
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @return string
     */
    public static function get_random_string(
        int $length,
        string $possible_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
        string $encoding = 'UTF-8'
    ): string {
        // init
        $i = 0;
        $str = '';

        //
        // add random chars
        //

        if ($encoding === 'UTF-8') {
            $max_length = (int) \mb_strlen($possible_chars);
            if ($max_length === 0) {
                return '';
            }

            while ($i < $length) {
                try {
                    $rand_int = \random_int(0, $max_length - 1);
                } catch (\Exception $e) {
                    $rand_int = \mt_rand(0, $max_length - 1);
                }
                $char = \mb_substr($possible_chars, $rand_int, 1);
                if ($char !== false) {
                    $str .= $char;
                    ++$i;
                }
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $max_length = (int) self::strlen($possible_chars, $encoding);
            if ($max_length === 0) {
                return '';
            }

            while ($i < $length) {
                try {
                    $rand_int = \random_int(0, $max_length - 1);
                } catch (\Exception $e) {
                    $rand_int = \mt_rand(0, $max_length - 1);
                }
                $char = self::substr($possible_chars, $rand_int, 1, $encoding);
                if ($char !== false) {
                    $str .= $char;
                    ++$i;
                }
            }
        }

        return $str;
    }

    /**
     * @param int|string $extra_entropy [optional] <p>Extra entropy via a string or int value.</p>
     * @param bool       $use_md5       [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
     *
     * @return string
     */
    public static function get_unique_string($extra_entropy = '', bool $use_md5 = true): string
    {
        try {
            $rand_int = \random_int(0, \mt_getrandmax());
        } catch (\Exception $e) {
            $rand_int = \mt_rand(0, \mt_getrandmax());
        }

        $unique_helper = $rand_int .
                         \session_id() .
                         ($_SERVER['REMOTE_ADDR'] ?? '') .
                         ($_SERVER['SERVER_ADDR'] ?? '') .
                         $extra_entropy;

        $unique_string = \uniqid($unique_helper, true);

        if ($use_md5) {
            $unique_string = \md5($unique_string . $unique_helper);
        }

        return $unique_string;
    }

    /**
     * Returns true if the string contains a lower case char, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not the string contains a lower case character.</p>
     */
    public static function has_lowercase(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('.*[[:lower:]]', $str);
        }

        return self::str_matches_pattern($str, '.*[[:lower:]]');
    }

    /**
     * Returns true if the string contains whitespace, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not the string contains whitespace.</p>
     */
    public static function has_whitespace(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('.*[[:space:]]', $str);
        }

        return self::str_matches_pattern($str, '.*[[:space:]]');
    }

    /**
     * Returns true if the string contains an upper case char, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not the string contains an upper case character.</p>
     */
    public static function has_uppercase(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('.*[[:upper:]]', $str);
        }

        return self::str_matches_pattern($str, '.*[[:upper:]]');
    }

    /**
     * Converts a hexadecimal value into a UTF-8 character.
     *
     * INFO: opposite to UTF8::chr_to_hex()
     *
     * EXAMPLE: <code>UTF8::hex_to_chr('U+00a7'); // '§'</code>
     *
     * @param string $hexdec <p>The hexadecimal value.</p>
     *
     * @psalm-pure
     *
     * @return false|string one single UTF-8 character
     */
    public static function hex_to_chr(string $hexdec)
    {
        /** @noinspection PhpUsageOfSilenceOperatorInspection - Invalid characters passed for attempted conversion, these have been ignored */
        return self::decimal_to_chr((int) @\hexdec($hexdec));
    }

    /**
     * Converts hexadecimal U+xxxx code point representation to integer.
     *
     * INFO: opposite to UTF8::int_to_hex()
     *
     * EXAMPLE: <code>UTF8::hex_to_int('U+00f1'); // 241</code>
     *
     * @param string $hexdec <p>The hexadecimal code point representation.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The code point, or false on failure.</p>
     */
    public static function hex_to_int($hexdec)
    {
        // init
        $hexdec = (string) $hexdec;

        if ($hexdec === '') {
            return false;
        }

        if (\preg_match('/^(?:\\\u|U\+|)([a-zA-Z0-9]{4,6})$/', $hexdec, $match)) {
            return \intval($match[1], 16);
        }

        return false;
    }

    /**
     * Converts a UTF-8 string to a series of HTML numbered entities.
     *
     * INFO: opposite to UTF8::html_decode()
     *
     * EXAMPLE: <code>UTF8::html_encode('中文空白'); // '&#20013;&#25991;&#31354;&#30333;'</code>
     *
     * @param string $str              <p>The Unicode string to be encoded as numbered entities.</p>
     * @param bool   $keep_ascii_chars [optional] <p>Keep ASCII chars.</p>
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string HTML numbered entities
     */
    public static function html_encode(
        string $str,
        bool $keep_ascii_chars = false,
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '') {
            return '';
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
        if (self::$SUPPORT['mbstring'] === true) {
            if ($keep_ascii_chars) {
                $start_code = 0x80;
            } else {
                $start_code = 0x00;
            }

            if ($encoding === 'UTF-8') {
                /** @var false|string|null $return - needed for PhpStan (stubs error) */
                $return = \mb_encode_numericentity(
                    $str,
                    [$start_code, 0xfffff, 0, 0xfffff]
                );
                if ($return !== null && $return !== false) {
                    return $return;
                }
            }

            /** @var false|string|null $return - needed for PhpStan (stubs error) */
            $return = \mb_encode_numericentity(
                $str,
                [$start_code, 0xfffff, 0, 0xfffff],
                $encoding
            );
            if ($return !== null && $return !== false) {
                return $return;
            }
        }

        //
        // fallback via vanilla php
        //

        return \implode(
            '',
            \array_map(
                static function (string $chr) use ($keep_ascii_chars, $encoding): string {
                    return self::single_chr_html_encode($chr, $keep_ascii_chars, $encoding);
                },
                self::str_split($str)
            )
        );
    }

    /**
     * UTF-8 version of html_entity_decode()
     *
     * The reason we are not using html_entity_decode() by itself is because
     * while it is not technically correct to leave out the semicolon
     * at the end of an entity most browsers will still interpret the entity
     * correctly. html_entity_decode() does not convert entities without
     * semicolons, so we are left with our own little solution here. Bummer.
     *
     * Convert all HTML entities to their applicable characters.
     *
     * INFO: opposite to UTF8::html_encode()
     *
     * EXAMPLE: <code>UTF8::html_entity_decode('&#20013;&#25991;&#31354;&#30333;'); // '中文空白'</code>
     *
     * @see http://php.net/manual/en/function.html-entity-decode.php
     *
     * @param string   $str      <p>
     *                           The input string.
     *                           </p>
     * @param int|null $flags    [optional] <p>
     *                           A bitmask of one or more of the following flags, which specify how to handle quotes
     *                           and which document type to use. The default is ENT_COMPAT | ENT_HTML401.
     *                           <table>
     *                           Available <i>flags</i> constants
     *                           <tr valign="top">
     *                           <td>Constant Name</td>
     *                           <td>Description</td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_COMPAT</b></td>
     *                           <td>Will convert double-quotes and leave single-quotes alone.</td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_QUOTES</b></td>
     *                           <td>Will convert both double and single quotes.</td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_NOQUOTES</b></td>
     *                           <td>Will leave both double and single quotes unconverted.</td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_HTML401</b></td>
     *                           <td>
     *                           Handle code as HTML 4.01.
     *                           </td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_XML1</b></td>
     *                           <td>
     *                           Handle code as XML 1.
     *                           </td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_XHTML</b></td>
     *                           <td>
     *                           Handle code as XHTML.
     *                           </td>
     *                           </tr>
     *                           <tr valign="top">
     *                           <td><b>ENT_HTML5</b></td>
     *                           <td>
     *                           Handle code as HTML 5.
     *                           </td>
     *                           </tr>
     *                           </table>
     *                           </p>
     * @param string   $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string the decoded string
     */
    public static function html_entity_decode(
        string $str,
        int $flags = null,
        string $encoding = 'UTF-8'
    ): string {
        if (
            !isset($str[3]) // examples: &; || &x;
            ||
            \strpos($str, '&') === false // no "&"
        ) {
            return $str;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($flags === null) {
            $flags = \ENT_QUOTES | \ENT_HTML5;
        }

        if (
            $encoding !== 'UTF-8'
            &&
            $encoding !== 'ISO-8859-1'
            &&
            $encoding !== 'WINDOWS-1252'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::html_entity_decode() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        do {
            $str_compare = $str;

            if (\strpos($str, '&') !== false) {
                if (\strpos($str, '&#') !== false) {
                    // decode also numeric & UTF16 two byte entities
                    $str = (string) \preg_replace(
                        '/(&#(?:x0*[0-9a-fA-F]{2,6}(?![0-9a-fA-F;])|(?:0*\d{2,6}(?![0-9;]))))/S',
                        '$1;',
                        $str
                    );
                }

                $str = \html_entity_decode(
                    $str,
                    $flags,
                    $encoding
                );
            }
        } while ($str_compare !== $str);

        return $str;
    }

    /**
     * Create a escape html version of the string via "UTF8::htmlspecialchars()".
     *
     * @param string $str
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function html_escape(string $str, string $encoding = 'UTF-8'): string
    {
        return self::htmlspecialchars(
            $str,
            \ENT_QUOTES | \ENT_SUBSTITUTE,
            $encoding
        );
    }

    /**
     * Remove empty html-tag.
     *
     * e.g.: <pre><tag></tag></pre>
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function html_stripe_empty_tags(string $str): string
    {
        return (string) \preg_replace(
            '/<[^\\/>]*?>\\s*?<\\/[^>]*?>/u',
            '',
            $str
        );
    }

    /**
     * Convert all applicable characters to HTML entities: UTF-8 version of htmlentities().
     *
     * EXAMPLE: <code>UTF8::htmlentities('<白-öäü>'); // '&lt;&#30333;-&ouml;&auml;&uuml;&gt;'</code>
     *
     * @see http://php.net/manual/en/function.htmlentities.php
     *
     * @param string $str           <p>
     *                              The input string.
     *                              </p>
     * @param int    $flags         [optional] <p>
     *                              A bitmask of one or more of the following flags, which specify how to handle
     *                              quotes, invalid code unit sequences and the used document type. The default is
     *                              ENT_COMPAT | ENT_HTML401.
     *                              <table>
     *                              Available <i>flags</i> constants
     *                              <tr valign="top">
     *                              <td>Constant Name</td>
     *                              <td>Description</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_COMPAT</b></td>
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_QUOTES</b></td>
     *                              <td>Will convert both double and single quotes.</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_NOQUOTES</b></td>
     *                              <td>Will leave both double and single quotes unconverted.</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_IGNORE</b></td>
     *                              <td>
     *                              Silently discard invalid code unit sequences instead of returning
     *                              an empty string. Using this flag is discouraged as it
     *                              may have security implications.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_SUBSTITUTE</b></td>
     *                              <td>
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
     *                              string.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_DISALLOWED</b></td>
     *                              <td>
     *                              Replace invalid code points for the given document type with a
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
     *                              (otherwise) instead of leaving them as is. This may be useful, for
     *                              instance, to ensure the well-formedness of XML documents with
     *                              embedded external content.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_HTML401</b></td>
     *                              <td>
     *                              Handle code as HTML 4.01.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_XML1</b></td>
     *                              <td>
     *                              Handle code as XML 1.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_XHTML</b></td>
     *                              <td>
     *                              Handle code as XHTML.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_HTML5</b></td>
     *                              <td>
     *                              Handle code as HTML 5.
     *                              </td>
     *                              </tr>
     *                              </table>
     *                              </p>
     * @param string $encoding      [optional] <p>
     *                              Like <b>htmlspecialchars</b>,
     *                              <b>htmlentities</b> takes an optional third argument
     *                              <i>encoding</i> which defines encoding used in
     *                              conversion.
     *                              Although this argument is technically optional, you are highly
     *                              encouraged to specify the correct value for your code.
     *                              </p>
     * @param bool   $double_encode [optional] <p>
     *                              When <i>double_encode</i> is turned off PHP will not
     *                              encode existing html entities. The default is to convert everything.
     *                              </p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>
     *                The encoded string.
     *                <br><br>
     *                If the input <i>string</i> contains an invalid code unit
     *                sequence within the given <i>encoding</i> an empty string
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
     *                <b>ENT_SUBSTITUTE</b> flags are set.
     *                </p>
     */
    public static function htmlentities(
        string $str,
        int $flags = \ENT_COMPAT,
        string $encoding = 'UTF-8',
        bool $double_encode = true
    ): string {
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        $str = \htmlentities(
            $str,
            $flags,
            $encoding,
            $double_encode
        );

        /**
         * PHP doesn't replace a backslash to its html entity since this is something
         * that's mostly used to escape characters when inserting in a database. Since
         * we're using a decent database layer, we don't need this shit and we're replacing
         * the double backslashes by its' html entity equivalent.
         *
         * https://github.com/forkcms/library/blob/master/spoon/filter/filter.php#L303
         */
        $str = \str_replace('\\', '&#92;', $str);

        return self::html_encode($str, true, $encoding);
    }

    /**
     * Convert only special characters to HTML entities: UTF-8 version of htmlspecialchars()
     *
     * INFO: Take a look at "UTF8::htmlentities()"
     *
     * EXAMPLE: <code>UTF8::htmlspecialchars('<白-öäü>'); // '&lt;白-öäü&gt;'</code>
     *
     * @see http://php.net/manual/en/function.htmlspecialchars.php
     *
     * @param string $str           <p>
     *                              The string being converted.
     *                              </p>
     * @param int    $flags         [optional] <p>
     *                              A bitmask of one or more of the following flags, which specify how to handle
     *                              quotes, invalid code unit sequences and the used document type. The default is
     *                              ENT_COMPAT | ENT_HTML401.
     *                              <table>
     *                              Available <i>flags</i> constants
     *                              <tr valign="top">
     *                              <td>Constant Name</td>
     *                              <td>Description</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_COMPAT</b></td>
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_QUOTES</b></td>
     *                              <td>Will convert both double and single quotes.</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_NOQUOTES</b></td>
     *                              <td>Will leave both double and single quotes unconverted.</td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_IGNORE</b></td>
     *                              <td>
     *                              Silently discard invalid code unit sequences instead of returning
     *                              an empty string. Using this flag is discouraged as it
     *                              may have security implications.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_SUBSTITUTE</b></td>
     *                              <td>
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
     *                              string.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_DISALLOWED</b></td>
     *                              <td>
     *                              Replace invalid code points for the given document type with a
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
     *                              (otherwise) instead of leaving them as is. This may be useful, for
     *                              instance, to ensure the well-formedness of XML documents with
     *                              embedded external content.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_HTML401</b></td>
     *                              <td>
     *                              Handle code as HTML 4.01.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_XML1</b></td>
     *                              <td>
     *                              Handle code as XML 1.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_XHTML</b></td>
     *                              <td>
     *                              Handle code as XHTML.
     *                              </td>
     *                              </tr>
     *                              <tr valign="top">
     *                              <td><b>ENT_HTML5</b></td>
     *                              <td>
     *                              Handle code as HTML 5.
     *                              </td>
     *                              </tr>
     *                              </table>
     *                              </p>
     * @param string $encoding      [optional] <p>
     *                              Defines encoding used in conversion.
     *                              </p>
     *                              <p>
     *                              For the purposes of this function, the encodings
     *                              ISO-8859-1, ISO-8859-15,
     *                              UTF-8, cp866,
     *                              cp1251, cp1252, and
     *                              KOI8-R are effectively equivalent, provided the
     *                              <i>string</i> itself is valid for the encoding, as
     *                              the characters affected by <b>htmlspecialchars</b> occupy
     *                              the same positions in all of these encodings.
     *                              </p>
     * @param bool   $double_encode [optional] <p>
     *                              When <i>double_encode</i> is turned off PHP will not
     *                              encode existing html entities, the default is to convert everything.
     *                              </p>
     *
     * @psalm-pure
     *
     * @return string the converted string.
     *                </p>
     *                <p>
     *                If the input <i>string</i> contains an invalid code unit
     *                sequence within the given <i>encoding</i> an empty string
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
     *                <b>ENT_SUBSTITUTE</b> flags are set
     */
    public static function htmlspecialchars(
        string $str,
        int $flags = \ENT_COMPAT,
        string $encoding = 'UTF-8',
        bool $double_encode = true
    ): string {
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        return \htmlspecialchars(
            $str,
            $flags,
            $encoding,
            $double_encode
        );
    }

    /**
     * Checks whether iconv is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function iconv_loaded(): bool
    {
        return \extension_loaded('iconv');
    }

    /**
     * Converts Integer to hexadecimal U+xxxx code point representation.
     *
     * INFO: opposite to UTF8::hex_to_int()
     *
     * EXAMPLE: <code>UTF8::int_to_hex(241); // 'U+00f1'</code>
     *
     * @param int    $int    <p>The integer to be converted to hexadecimal code point.</p>
     * @param string $prefix [optional]
     *
     * @psalm-pure
     *
     * @return string the code point, or empty string on failure
     */
    public static function int_to_hex(int $int, string $prefix = 'U+'): string
    {
        $hex = \dechex($int);

        $hex = (\strlen($hex) < 4 ? \substr('0000' . $hex, -4) : $hex);

        return $prefix . $hex . '';
    }

    /**
     * Checks whether intl-char is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function intlChar_loaded(): bool
    {
        return \class_exists('IntlChar');
    }

    /**
     * Checks whether intl is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function intl_loaded(): bool
    {
        return \extension_loaded('intl');
    }

    /**
     * Returns true if the string contains only alphabetic chars, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only alphabetic chars.</p>
     */
    public static function is_alpha(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('^[[:alpha:]]*$', $str);
        }

        return self::str_matches_pattern($str, '^[[:alpha:]]*$');
    }

    /**
     * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only alphanumeric chars.</p>
     */
    public static function is_alphanumeric(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('^[[:alnum:]]*$', $str);
        }

        return self::str_matches_pattern($str, '^[[:alnum:]]*$');
    }

    /**
     * Returns true if the string contains only punctuation chars, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only punctuation chars.</p>
     */
    public static function is_punctuation(string $str): bool
    {
        return self::str_matches_pattern($str, '^[[:punct:]]*$');
    }

    /**
     * Returns true if the string contains only printable (non-invisible) chars, false otherwise.
     *
     * @param string $str                       <p>The input string.</p>
     * @param bool   $ignore_control_characters [optional] <p>Ignore control characters like [LRM] or [LSEP].</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only printable (non-invisible) chars.</p>
     */
    public static function is_printable(string $str, bool $ignore_control_characters = false): bool
    {
        return self::remove_invisible_characters($str, false, '', $ignore_control_characters) === $str;
    }

    /**
     * Checks if a string is 7 bit ASCII.
     *
     * EXAMPLE: <code>UTF8::is_ascii('白'); // false</code>
     *
     * @param string $str <p>The string to check.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>
     *              <strong>true</strong> if it is ASCII<br>
     *              <strong>false</strong> otherwise
     *              </p>
     */
    public static function is_ascii(string $str): bool
    {
        return ASCII::is_ascii($str);
    }

    /**
     * Returns true if the string is base64 encoded, false otherwise.
     *
     * EXAMPLE: <code>UTF8::is_base64('4KSu4KWL4KSo4KS/4KSa'); // true</code>
     *
     * @param string|null $str                   <p>The input string.</p>
     * @param bool        $empty_string_is_valid [optional] <p>Is an empty string valid base64 or not?</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str is base64 encoded.</p>
     */
    public static function is_base64($str, bool $empty_string_is_valid = false): bool
    {
        if (
            !$empty_string_is_valid
            &&
            $str === ''
        ) {
            return false;
        }

        if (!\is_string($str)) {
            return false;
        }

        $base64String = \base64_decode($str, true);

        return $base64String !== false && \base64_encode($base64String) === $str;
    }

    /**
     * Check if the input is binary... (is look like a hack).
     *
     * EXAMPLE: <code>UTF8::is_binary(01); // true</code>
     *
     * @param int|string $input
     * @param bool       $strict
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function is_binary($input, bool $strict = false): bool
    {
        $input = (string) $input;
        if ($input === '') {
            return false;
        }

        if (\preg_match('~^[01]+$~', $input)) {
            return true;
        }

        $ext = self::get_file_type($input);
        if ($ext['type'] === 'binary') {
            return true;
        }

        if (!$strict) {
            $test_length = \strlen($input);
            $test_null_counting = \substr_count($input, "\x0", 0, $test_length);
            if (($test_null_counting / $test_length) > 0.25) {
                return true;
            }
        }

        if ($strict) {
            if (self::$SUPPORT['finfo'] === false) {
                throw new \RuntimeException('ext-fileinfo: is not installed');
            }

            /**
             * @psalm-suppress ImpureMethodCall - it will return the same result for the same file ...
             */
            $finfo_encoding = (new \finfo(\FILEINFO_MIME_ENCODING))->buffer($input);
            if ($finfo_encoding && $finfo_encoding === 'binary') {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if the file is binary.
     *
     * EXAMPLE: <code>UTF8::is_binary('./utf32.txt'); // true</code>
     *
     * @param string $file
     *
     * @return bool
     */
    public static function is_binary_file($file): bool
    {
        // init
        $block = '';

        $fp = \fopen($file, 'rb');
        if (\is_resource($fp)) {
            $block = \fread($fp, 512);
            \fclose($fp);
        }

        if ($block === '' || $block === false) {
            return false;
        }

        return self::is_binary($block, true);
    }

    /**
     * Returns true if the string contains only whitespace chars, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only whitespace characters.</p>
     */
    public static function is_blank(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('^[[:space:]]*$', $str);
        }

        return self::str_matches_pattern($str, '^[[:space:]]*$');
    }

    /**
     * Checks if the given string is equal to any "Byte Order Mark".
     *
     * WARNING: Use "UTF8::string_has_bom()" if you will check BOM in a string.
     *
     * EXAMPLE: <code>UTF8::is_bom("\xef\xbb\xbf"); // true</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if the $utf8_chr is Byte Order Mark, <strong>false</strong> otherwise.</p>
     */
    public static function is_bom($str): bool
    {
        /** @noinspection PhpUnusedLocalVariableInspection */
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
            if ($str === $bom_string) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine whether the string is considered to be empty.
     *
     * A variable is considered empty if it does not exist or if its value equals FALSE.
     * empty() does not generate a warning if the variable does not exist.
     *
     * @param array|float|int|string $str
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str is empty().</p>
     */
    public static function is_empty($str): bool
    {
        return empty($str);
    }

    /**
     * Returns true if the string contains only hexadecimal chars, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only hexadecimal chars.</p>
     */
    public static function is_hexadecimal(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('^[[:xdigit:]]*$', $str);
        }

        return self::str_matches_pattern($str, '^[[:xdigit:]]*$');
    }

    /**
     * Check if the string contains any HTML tags.
     *
     * EXAMPLE: <code>UTF8::is_html('<b>lall</b>'); // true</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains html elements.</p>
     */
    public static function is_html(string $str): bool
    {
        if ($str === '') {
            return false;
        }

        // init
        $matches = [];

        $str = self::emoji_encode($str); // hack for emoji support :/

        \preg_match("/<\\/?\\w+(?:(?:\\s+\\w+(?:\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)*\\s*|\\s*)\\/?>/u", $str, $matches);

        return $matches !== [];
    }

    /**
     * Check if $url is an correct url.
     *
     * @param string $url
     * @param bool   $disallow_localhost
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function is_url(string $url, bool $disallow_localhost = false): bool
    {
        if ($url === '') {
            return false;
        }

        // WARNING: keep this as hack protection
        if (!self::str_istarts_with_any($url, ['http://', 'https://'])) {
            return false;
        }

        // e.g. -> the server itself connect to "https://foo.localhost/phpmyadmin/...
        if ($disallow_localhost) {
            if (self::str_istarts_with_any(
                $url,
                [
                    'http://localhost',
                    'https://localhost',
                    'http://127.0.0.1',
                    'https://127.0.0.1',
                    'http://::1',
                    'https://::1',
                ]
            )) {
                return false;
            }

            $regex = '/^(?:http(?:s)?:\/\/).*?(?:\.localhost)/iu';
            if (\preg_match($regex, $url)) {
                return false;
            }
        }

        // INFO: this is needed for e.g. "http://müller.de/" (internationalized domain names) and non ASCII-parameters
        $regex = '/^(?:http(?:s)?:\\/\\/)(?:[\p{L}0-9][\p{L}0-9_-]*(?:\\.[\p{L}0-9][\p{L}0-9_-]*))(?:\\d+)?(?:\\/\\.*)?/iu';
        if (\preg_match($regex, $url)) {
            return true;
        }

        return \filter_var($url, \FILTER_VALIDATE_URL) !== false;
    }

    /**
     * Try to check if "$str" is a JSON-string.
     *
     * EXAMPLE: <code>UTF8::is_json('{"array":[1,"¥","ä"]}'); // true</code>
     *
     * @param string $str                                    <p>The input string.</p>
     * @param bool   $only_array_or_object_results_are_valid [optional] <p>Only array and objects are valid json
     *                                                       results.</p>
     *
     * @return bool
     *              <p>Whether or not the $str is in JSON format.</p>
     */
    public static function is_json(string $str, bool $only_array_or_object_results_are_valid = true): bool
    {
        if ($str === '') {
            return false;
        }

        if (self::$SUPPORT['json'] === false) {
            throw new \RuntimeException('ext-json: is not installed');
        }

        $jsonOrNull = self::json_decode($str);
        if ($jsonOrNull === null && \strtoupper($str) !== 'NULL') {
            return false;
        }

        if (
            $only_array_or_object_results_are_valid
            &&
            !\is_object($jsonOrNull)
            &&
            !\is_array($jsonOrNull)
        ) {
            return false;
        }

        return \json_last_error() === \JSON_ERROR_NONE;
    }

    /**
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only lowercase chars.</p>
     */
    public static function is_lowercase(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('^[[:lower:]]*$', $str);
        }

        return self::str_matches_pattern($str, '^[[:lower:]]*$');
    }

    /**
     * Returns true if the string is serialized, false otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str is serialized.</p>
     */
    public static function is_serialized(string $str): bool
    {
        if ($str === '') {
            return false;
        }

        /** @noinspection PhpUsageOfSilenceOperatorInspection */
        /** @noinspection UnserializeExploitsInspection */
        return $str === 'b:0;'
               ||
               @\unserialize($str, []) !== false;
    }

    /**
     * Returns true if the string contains only lower case chars, false
     * otherwise.
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains only lower case characters.</p>
     */
    public static function is_uppercase(string $str): bool
    {
        if (self::$SUPPORT['mbstring'] === true) {
            return \mb_ereg_match('^[[:upper:]]*$', $str);
        }

        return self::str_matches_pattern($str, '^[[:upper:]]*$');
    }

    /**
     * Check if the string is UTF-16.
     *
     * EXAMPLE: <code>
     * UTF8::is_utf16(file_get_contents('utf-16-le.txt')); // 1
     * //
     * UTF8::is_utf16(file_get_contents('utf-16-be.txt')); // 2
     * //
     * UTF8::is_utf16(file_get_contents('utf-8.txt')); // false
     * </code>
     *
     * @param string $str                       <p>The input string.</p>
     * @param bool   $check_if_string_is_binary
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <strong>false</strong> if is't not UTF-16,<br>
     *                   <strong>1</strong> for UTF-16LE,<br>
     *                   <strong>2</strong> for UTF-16BE
     */
    public static function is_utf16($str, bool $check_if_string_is_binary = true)
    {
        // init
        $str = (string) $str;
        $str_chars = [];

        // fix for the "binary"-check
        if ($check_if_string_is_binary !== false && self::string_has_bom($str)) {
            $check_if_string_is_binary = false;
        }

        if (
            $check_if_string_is_binary
            &&
            !self::is_binary($str, true)
        ) {
            return false;
        }

        if (self::$SUPPORT['mbstring'] === false) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::is_utf16() without mbstring may did not work correctly', \E_USER_WARNING);
        }

        $str = self::remove_bom($str);

        $maybe_utf16le = 0;
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16LE');
        if ($test) {
            $test2 = \mb_convert_encoding($test, 'UTF-16LE', 'UTF-8');
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16LE');
            if ($test3 === $test) {
                /**
                 * @psalm-suppress RedundantCondition
                 */
                if ($str_chars === []) {
                    $str_chars = self::count_chars($str, true, false);
                }
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
                    if (\in_array($test3char, $str_chars, true)) {
                        ++$maybe_utf16le;
                    }
                }
                unset($test3charEmpty);
            }
        }

        $maybe_utf16be = 0;
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
        if ($test) {
            $test2 = \mb_convert_encoding($test, 'UTF-16BE', 'UTF-8');
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16BE');
            if ($test3 === $test) {
                if ($str_chars === []) {
                    $str_chars = self::count_chars($str, true, false);
                }
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
                    if (\in_array($test3char, $str_chars, true)) {
                        ++$maybe_utf16be;
                    }
                }
                unset($test3charEmpty);
            }
        }

        if ($maybe_utf16be !== $maybe_utf16le) {
            if ($maybe_utf16le > $maybe_utf16be) {
                return 1;
            }

            return 2;
        }

        return false;
    }

    /**
     * Check if the string is UTF-32.
     *
     * EXAMPLE: <code>
     * UTF8::is_utf32(file_get_contents('utf-32-le.txt')); // 1
     * //
     * UTF8::is_utf32(file_get_contents('utf-32-be.txt')); // 2
     * //
     * UTF8::is_utf32(file_get_contents('utf-8.txt')); // false
     * </code>
     *
     * @param string $str                       <p>The input string.</p>
     * @param bool   $check_if_string_is_binary
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <strong>false</strong> if is't not UTF-32,<br>
     *                   <strong>1</strong> for UTF-32LE,<br>
     *                   <strong>2</strong> for UTF-32BE
     */
    public static function is_utf32($str, bool $check_if_string_is_binary = true)
    {
        // init
        $str = (string) $str;
        $str_chars = [];

        // fix for the "binary"-check
        if ($check_if_string_is_binary !== false && self::string_has_bom($str)) {
            $check_if_string_is_binary = false;
        }

        if (
            $check_if_string_is_binary
            &&
            !self::is_binary($str, true)
        ) {
            return false;
        }

        if (self::$SUPPORT['mbstring'] === false) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::is_utf32() without mbstring may did not work correctly', \E_USER_WARNING);
        }

        $str = self::remove_bom($str);

        $maybe_utf32le = 0;
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32LE');
        if ($test) {
            $test2 = \mb_convert_encoding($test, 'UTF-32LE', 'UTF-8');
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32LE');
            if ($test3 === $test) {
                /**
                 * @psalm-suppress RedundantCondition
                 */
                if ($str_chars === []) {
                    $str_chars = self::count_chars($str, true, false);
                }
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
                    if (\in_array($test3char, $str_chars, true)) {
                        ++$maybe_utf32le;
                    }
                }
                unset($test3charEmpty);
            }
        }

        $maybe_utf32be = 0;
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32BE');
        if ($test) {
            $test2 = \mb_convert_encoding($test, 'UTF-32BE', 'UTF-8');
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32BE');
            if ($test3 === $test) {
                if ($str_chars === []) {
                    $str_chars = self::count_chars($str, true, false);
                }
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
                    if (\in_array($test3char, $str_chars, true)) {
                        ++$maybe_utf32be;
                    }
                }
                unset($test3charEmpty);
            }
        }

        if ($maybe_utf32be !== $maybe_utf32le) {
            if ($maybe_utf32le > $maybe_utf32be) {
                return 1;
            }

            return 2;
        }

        return false;
    }

    /**
     * Checks whether the passed input contains only byte sequences that appear valid UTF-8.
     *
     * EXAMPLE: <code>
     * UTF8::is_utf8(['Iñtërnâtiônàlizætiøn', 'foo']); // true
     * //
     * UTF8::is_utf8(["Iñtërnâtiônàlizætiøn\xA0\xA1", 'bar']); // false
     * </code>
     *
     * @param int|string|string[]|null $str    <p>The input to be checked.</p>
     * @param bool                     $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function is_utf8($str, bool $strict = false): bool
    {
        if (\is_array($str)) {
            foreach ($str as &$v) {
                if (!self::is_utf8($v, $strict)) {
                    return false;
                }
            }

            return true;
        }

        return self::is_utf8_string((string) $str, $strict);
    }

    /**
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
     * Decodes a JSON string
     *
     * EXAMPLE: <code>UTF8::json_decode('[1,"\u00a5","\u00e4"]'); // array(1, '¥', 'ä')</code>
     *
     * @see http://php.net/manual/en/function.json-decode.php
     *
     * @param string $json    <p>
     *                        The <i>json</i> string being decoded.
     *                        </p>
     *                        <p>
     *                        This function only works with UTF-8 encoded strings.
     *                        </p>
     *                        <p>PHP implements a superset of
     *                        JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
     *                        only supports these values when they are nested inside an array or an object.
     *                        </p>
     * @param bool   $assoc   [optional] <p>
     *                        When <b>TRUE</b>, returned objects will be converted into
     *                        associative arrays.
     *                        </p>
     * @param int    $depth   [optional] <p>
     *                        User specified recursion depth.
     *                        </p>
     * @param int    $options [optional] <p>
     *                        Bitmask of JSON decode options. Currently only
     *                        <b>JSON_BIGINT_AS_STRING</b>
     *                        is supported (default is to cast large integers as floats)
     *                        </p>
     *
     * @psalm-pure
     *
     * @return mixed
     *               <p>The value encoded in <i>json</i> in appropriate PHP type. Values true, false and
     *               null (case-insensitive) are returned as <b>TRUE</b>, <b>FALSE</b> and <b>NULL</b> respectively.
     *               <b>NULL</b> is returned if the <i>json</i> cannot be decoded or if the encoded data
     *               is deeper than the recursion limit.</p>
     */
    public static function json_decode(
        string $json,
        bool $assoc = false,
        int $depth = 512,
        int $options = 0
    ) {
        $json = self::filter($json);

        if (self::$SUPPORT['json'] === false) {
            throw new \RuntimeException('ext-json: is not installed');
        }

        if ($depth < 1) {
            $depth = 1;
        }

        return \json_decode($json, $assoc, $depth, $options);
    }

    /**
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
     * Returns the JSON representation of a value.
     *
     * EXAMPLE: <code>UTF8::json_enocde(array(1, '¥', 'ä')); // '[1,"\u00a5","\u00e4"]'</code>
     *
     * @see http://php.net/manual/en/function.json-encode.php
     *
     * @param mixed $value   <p>
     *                       The <i>value</i> being encoded. Can be any type except
     *                       a resource.
     *                       </p>
     *                       <p>
     *                       All string data must be UTF-8 encoded.
     *                       </p>
     *                       <p>PHP implements a superset of
     *                       JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
     *                       only supports these values when they are nested inside an array or an object.
     *                       </p>
     * @param int   $options [optional] <p>
     *                       Bitmask consisting of <b>JSON_HEX_QUOT</b>,
     *                       <b>JSON_HEX_TAG</b>,
     *                       <b>JSON_HEX_AMP</b>,
     *                       <b>JSON_HEX_APOS</b>,
     *                       <b>JSON_NUMERIC_CHECK</b>,
     *                       <b>JSON_PRETTY_PRINT</b>,
     *                       <b>JSON_UNESCAPED_SLASHES</b>,
     *                       <b>JSON_FORCE_OBJECT</b>,
     *                       <b>JSON_UNESCAPED_UNICODE</b>. The behaviour of these
     *                       constants is described on
     *                       the JSON constants page.
     *                       </p>
     * @param int   $depth   [optional] <p>
     *                       Set the maximum depth. Must be greater than zero.
     *                       </p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      A JSON encoded <strong>string</strong> on success or<br>
     *                      <strong>FALSE</strong> on failure
     */
    public static function json_encode($value, int $options = 0, int $depth = 512)
    {
        $value = self::filter($value);

        if (self::$SUPPORT['json'] === false) {
            throw new \RuntimeException('ext-json: is not installed');
        }

        if ($depth < 1) {
            $depth = 1;
        }

        return \json_encode($value, $options, $depth);
    }

    /**
     * Checks whether JSON is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function json_loaded(): bool
    {
        return \function_exists('json_decode');
    }

    /**
     * Makes string's first char lowercase.
     *
     * EXAMPLE: <code>UTF8::lcfirst('ÑTËRNÂTIÔNÀLIZÆTIØN'); // ñTËRNÂTIÔNÀLIZÆTIØN</code>
     *
     * @param string      $str                           <p>The input string</p>
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string the resulting string
     */
    public static function lcfirst(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        if ($clean_utf8) {
            $str = self::clean($str);
        }

        $use_mb_functions = ($lang === null && !$try_to_keep_the_string_length);

        if ($encoding === 'UTF-8') {
            $str_part_two = (string) \mb_substr($str, 1);

            if ($use_mb_functions) {
                $str_part_one = \mb_strtolower(
                    (string) \mb_substr($str, 0, 1)
                );
            } else {
                $str_part_one = self::strtolower(
                    (string) \mb_substr($str, 0, 1),
                    $encoding,
                    false,
                    $lang,
                    $try_to_keep_the_string_length
                );
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $str_part_two = (string) self::substr($str, 1, null, $encoding);

            $str_part_one = self::strtolower(
                (string) self::substr($str, 0, 1, $encoding),
                $encoding,
                false,
                $lang,
                $try_to_keep_the_string_length
            );
        }

        return $str_part_one . $str_part_two;
    }

    /**
     * Lowercase for all words in the string.
     *
     * @param string      $str                           <p>The input string.</p>
     * @param string[]    $exceptions                    [optional] <p>Exclusion for some words.</p>
     * @param string      $char_list                     [optional] <p>Additional chars that contains to words and do
     *                                                   not start a new word.</p>
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function lcwords(
        string $str,
        array $exceptions = [],
        string $char_list = '',
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        if (!$str) {
            return '';
        }

        $words = self::str_to_words($str, $char_list);
        $use_exceptions = $exceptions !== [];

        $words_str = '';
        foreach ($words as &$word) {
            if (!$word) {
                continue;
            }

            if (
                !$use_exceptions
                ||
                !\in_array($word, $exceptions, true)
            ) {
                $words_str .= self::lcfirst($word, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
            } else {
                $words_str .= $word;
            }
        }

        return $words_str;
    }

    /**
     * Strip whitespace or other characters from the beginning of a UTF-8 string.
     *
     * EXAMPLE: <code>UTF8::ltrim(' 中文空白  '); // '中文空白  '</code>
     *
     * @param string      $str   <p>The string to be trimmed</p>
     * @param string|null $chars <p>Optional characters to be stripped</p>
     *
     * @psalm-pure
     *
     * @return string the string with unwanted characters stripped from the left
     */
    public static function ltrim(string $str = '', string $chars = null): string
    {
        if ($str === '') {
            return '';
        }

        if (self::$SUPPORT['mbstring'] === true) {
            if ($chars !== null) {
                /** @noinspection PregQuoteUsageInspection */
                $chars = \preg_quote($chars);
                $pattern = "^[${chars}]+";
            } else {
                $pattern = '^[\\s]+';
            }

            return (string) \mb_ereg_replace($pattern, '', $str);
        }

        if ($chars !== null) {
            $chars = \preg_quote($chars, '/');
            $pattern = "^[${chars}]+";
        } else {
            $pattern = '^[\\s]+';
        }

        return self::regex_replace($str, $pattern, '');
    }

    /**
     * Returns the UTF-8 character with the maximum code point in the given data.
     *
     * EXAMPLE: <code>UTF8::max('abc-äöü-中文空白'); // 'ø'</code>
     *
     * @param array<string>|string $arg <p>A UTF-8 encoded string or an array of such strings.</p>
     *
     * @psalm-pure
     *
     * @return string|null the character with the highest code point than others, returns null on failure or empty input
     */
    public static function max($arg)
    {
        if (\is_array($arg)) {
            $arg = \implode('', $arg);
        }

        $codepoints = self::codepoints($arg);
        if ($codepoints === []) {
            return null;
        }

        $codepoint_max = \max($codepoints);

        return self::chr((int) $codepoint_max);
    }

    /**
     * Calculates and returns the maximum number of bytes taken by any
     * UTF-8 encoded character in the given string.
     *
     * EXAMPLE: <code>UTF8::max_chr_width('Intërnâtiônàlizætiøn'); // 2</code>
     *
     * @param string $str <p>The original Unicode string.</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <p>Max byte lengths of the given chars.</p>
     */
    public static function max_chr_width(string $str): int
    {
        $bytes = self::chr_size_list($str);
        if ($bytes !== []) {
            return (int) \max($bytes);
        }

        return 0;
    }

    /**
     * Checks whether mbstring is available on the server.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if available, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function mbstring_loaded(): bool
    {
        return \extension_loaded('mbstring');
    }

    /**
     * Returns the UTF-8 character with the minimum code point in the given data.
     *
     * EXAMPLE: <code>UTF8::min('abc-äöü-中文空白'); // '-'</code>
     *
     * @param string|string[] $arg <strong>A UTF-8 encoded string or an array of such strings.</strong>
     *
     * @psalm-pure
     *
     * @return string|null
     *                     <p>The character with the lowest code point than others, returns null on failure or empty input.</p>
     */
    public static function min($arg)
    {
        if (\is_array($arg)) {
            $arg = \implode('', $arg);
        }

        $codepoints = self::codepoints($arg);
        if ($codepoints === []) {
            return null;
        }

        $codepoint_min = \min($codepoints);

        return self::chr((int) $codepoint_min);
    }

    /**
     * Normalize the encoding-"name" input.
     *
     * EXAMPLE: <code>UTF8::normalize_encoding('UTF8'); // 'UTF-8'</code>
     *
     * @param mixed $encoding <p>e.g.: ISO, UTF8, WINDOWS-1251 etc.</p>
     * @param mixed $fallback <p>e.g.: UTF-8</p>
     *
     * @psalm-pure
     *
     * @return mixed|string
     *                      <p>e.g.: ISO-8859-1, UTF-8, WINDOWS-1251 etc.<br>Will return a empty string as fallback (by default)</p>
     *
     * @template TNormalizeEncodingFallback
     * @phpstan-param string|TNormalizeEncodingFallback $fallback
     * @phpstan-return string|TNormalizeEncodingFallback
     */
    public static function normalize_encoding($encoding, $fallback = '')
    {
        /**
         * @psalm-suppress ImpureStaticVariable
         *
         * @var array<string,string>
         */
        static $STATIC_NORMALIZE_ENCODING_CACHE = [];

        // init
        $encoding = (string) $encoding;

        if (!$encoding) {
            return $fallback;
        }

        if (
            $encoding === 'UTF-8'
            ||
            $encoding === 'UTF8'
        ) {
            return 'UTF-8';
        }

        if (
            $encoding === '8BIT'
            ||
            $encoding === 'BINARY'
        ) {
            return 'CP850';
        }

        if (
            $encoding === 'HTML'
            ||
            $encoding === 'HTML-ENTITIES'
        ) {
            return 'HTML-ENTITIES';
        }

        if (
            $encoding === 'ISO'
            ||
            $encoding === 'ISO-8859-1'
        ) {
            return 'ISO-8859-1';
        }

        if (
            $encoding === '1' // only a fallback, for non "strict_types" usage ...
            ||
            $encoding === '0' // only a fallback, for non "strict_types" usage ...
        ) {
            return $fallback;
        }

        if (isset($STATIC_NORMALIZE_ENCODING_CACHE[$encoding])) {
            return $STATIC_NORMALIZE_ENCODING_CACHE[$encoding];
        }

        if (self::$ENCODINGS === null) {
            self::$ENCODINGS = self::getData('encodings');
        }

        if (\in_array($encoding, self::$ENCODINGS, true)) {
            $STATIC_NORMALIZE_ENCODING_CACHE[$encoding] = $encoding;

            return $encoding;
        }

        $encoding_original = $encoding;
        $encoding = \strtoupper($encoding);
        $encoding_upper_helper = (string) \preg_replace('/[^a-zA-Z0-9]/u', '', $encoding);

        $equivalences = [
            'ISO8859'     => 'ISO-8859-1',
            'ISO88591'    => 'ISO-8859-1',
            'ISO'         => 'ISO-8859-1',
            'LATIN'       => 'ISO-8859-1',
            'LATIN1'      => 'ISO-8859-1', // Western European
            'ISO88592'    => 'ISO-8859-2',
            'LATIN2'      => 'ISO-8859-2', // Central European
            'ISO88593'    => 'ISO-8859-3',
            'LATIN3'      => 'ISO-8859-3', // Southern European
            'ISO88594'    => 'ISO-8859-4',
            'LATIN4'      => 'ISO-8859-4', // Northern European
            'ISO88595'    => 'ISO-8859-5',
            'ISO88596'    => 'ISO-8859-6', // Greek
            'ISO88597'    => 'ISO-8859-7',
            'ISO88598'    => 'ISO-8859-8', // Hebrew
            'ISO88599'    => 'ISO-8859-9',
            'LATIN5'      => 'ISO-8859-9', // Turkish
            'ISO885911'   => 'ISO-8859-11',
            'TIS620'      => 'ISO-8859-11', // Thai
            'ISO885910'   => 'ISO-8859-10',
            'LATIN6'      => 'ISO-8859-10', // Nordic
            'ISO885913'   => 'ISO-8859-13',
            'LATIN7'      => 'ISO-8859-13', // Baltic
            'ISO885914'   => 'ISO-8859-14',
            'LATIN8'      => 'ISO-8859-14', // Celtic
            'ISO885915'   => 'ISO-8859-15',
            'LATIN9'      => 'ISO-8859-15', // Western European (with some extra chars e.g. €)
            'ISO885916'   => 'ISO-8859-16',
            'LATIN10'     => 'ISO-8859-16', // Southeast European
            'CP1250'      => 'WINDOWS-1250',
            'WIN1250'     => 'WINDOWS-1250',
            'WINDOWS1250' => 'WINDOWS-1250',
            'CP1251'      => 'WINDOWS-1251',
            'WIN1251'     => 'WINDOWS-1251',
            'WINDOWS1251' => 'WINDOWS-1251',
            'CP1252'      => 'WINDOWS-1252',
            'WIN1252'     => 'WINDOWS-1252',
            'WINDOWS1252' => 'WINDOWS-1252',
            'CP1253'      => 'WINDOWS-1253',
            'WIN1253'     => 'WINDOWS-1253',
            'WINDOWS1253' => 'WINDOWS-1253',
            'CP1254'      => 'WINDOWS-1254',
            'WIN1254'     => 'WINDOWS-1254',
            'WINDOWS1254' => 'WINDOWS-1254',
            'CP1255'      => 'WINDOWS-1255',
            'WIN1255'     => 'WINDOWS-1255',
            'WINDOWS1255' => 'WINDOWS-1255',
            'CP1256'      => 'WINDOWS-1256',
            'WIN1256'     => 'WINDOWS-1256',
            'WINDOWS1256' => 'WINDOWS-1256',
            'CP1257'      => 'WINDOWS-1257',
            'WIN1257'     => 'WINDOWS-1257',
            'WINDOWS1257' => 'WINDOWS-1257',
            'CP1258'      => 'WINDOWS-1258',
            'WIN1258'     => 'WINDOWS-1258',
            'WINDOWS1258' => 'WINDOWS-1258',
            'UTF16'       => 'UTF-16',
            'UTF32'       => 'UTF-32',
            'UTF8'        => 'UTF-8',
            'UTF'         => 'UTF-8',
            'UTF7'        => 'UTF-7',
            '8BIT'        => 'CP850',
            'BINARY'      => 'CP850',
        ];

        if (!empty($equivalences[$encoding_upper_helper])) {
            $encoding = $equivalences[$encoding_upper_helper];
        }

        $STATIC_NORMALIZE_ENCODING_CACHE[$encoding_original] = $encoding;

        return $encoding;
    }

    /**
     * Standardize line ending to unix-like.
     *
     * @param string          $str      <p>The input string.</p>
     * @param string|string[] $replacer <p>The replacer char e.g. "\n" (Linux) or "\r\n" (Windows). You can also use \PHP_EOL
     *                                  here.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with normalized line ending.</p>
     */
    public static function normalize_line_ending(string $str, $replacer = "\n"): string
    {
        return \str_replace(["\r\n", "\r", "\n"], $replacer, $str);
    }

    /**
     * Normalize some MS Word special characters.
     *
     * EXAMPLE: <code>UTF8::normalize_msword('„Abcdef…”'); // '"Abcdef..."'</code>
     *
     * @param string $str <p>The string to be normalized.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with normalized characters for commonly used chars in Word documents.</p>
     */
    public static function normalize_msword(string $str): string
    {
        return ASCII::normalize_msword($str);
    }

    /**
     * Normalize the whitespace.
     *
     * EXAMPLE: <code>UTF8::normalize_whitespace("abc-\xc2\xa0-öäü-\xe2\x80\xaf-\xE2\x80\xAC", true); // "abc-\xc2\xa0-öäü- -"</code>
     *
     * @param string $str                          <p>The string to be normalized.</p>
     * @param bool   $keep_non_breaking_space      [optional] <p>Set to true, to keep non-breaking-spaces.</p>
     * @param bool   $keep_bidi_unicode_controls   [optional] <p>Set to true, to keep non-printable (for the web)
     *                                             bidirectional text chars.</p>
     * @param bool   $normalize_control_characters [optional] <p>Set to true, to convert e.g. LINE-, PARAGRAPH-SEPARATOR with "\n" and LINE TABULATION with "\t".</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with normalized whitespace.</p>
     */
    public static function normalize_whitespace(
        string $str,
        bool $keep_non_breaking_space = false,
        bool $keep_bidi_unicode_controls = false,
        bool $normalize_control_characters = false
    ): string {
        return ASCII::normalize_whitespace(
            $str,
            $keep_non_breaking_space,
            $keep_bidi_unicode_controls,
            $normalize_control_characters
        );
    }

    /**
     * Calculates Unicode code point of the given UTF-8 encoded character.
     *
     * INFO: opposite to UTF8::chr()
     *
     * EXAMPLE: <code>UTF8::ord('☃'); // 0x2603</code>
     *
     * @param string $chr      <p>The character of which to calculate code point.<p/>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <p>Unicode code point of the given character,<br>
     *             0 on invalid UTF-8 byte sequence</p>
     */
    public static function ord($chr, string $encoding = 'UTF-8'): int
    {
        /**
         * @psalm-suppress ImpureStaticVariable
         *
         * @var array<string,int>
         */
        static $CHAR_CACHE = [];

        // init
        $chr = (string) $chr;

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        $cache_key = $chr . '_' . $encoding;
        if (isset($CHAR_CACHE[$cache_key])) {
            return $CHAR_CACHE[$cache_key];
        }

        // check again, if it's still not UTF-8
        if ($encoding !== 'UTF-8') {
            $chr = self::encode($encoding, $chr);
        }

        if (self::$ORD === null) {
            self::$ORD = self::getData('ord');
        }

        if (isset(self::$ORD[$chr])) {
            return $CHAR_CACHE[$cache_key] = self::$ORD[$chr];
        }

        //
        // fallback via "IntlChar"
        //

        if (self::$SUPPORT['intlChar'] === true) {
            $code = \IntlChar::ord($chr);
            if ($code) {
                return $CHAR_CACHE[$cache_key] = $code;
            }
        }

        //
        // fallback via vanilla php
        //

        $chr = \unpack('C*', (string) \substr($chr, 0, 4));
        /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
        /** @var int[] $chr - "unpack": only false if the format string contains errors */
        $chr = $chr;
        $code = $chr ? $chr[1] : 0;

        if ($code >= 0xF0 && isset($chr[4])) {
            return $CHAR_CACHE[$cache_key] = ((($code - 0xF0) << 18) + (($chr[2] - 0x80) << 12) + (($chr[3] - 0x80) << 6) + $chr[4] - 0x80);
        }

        if ($code >= 0xE0 && isset($chr[3])) {
            return $CHAR_CACHE[$cache_key] = ((($code - 0xE0) << 12) + (($chr[2] - 0x80) << 6) + $chr[3] - 0x80);
        }

        if ($code >= 0xC0 && isset($chr[2])) {
            return $CHAR_CACHE[$cache_key] = ((($code - 0xC0) << 6) + $chr[2] - 0x80);
        }

        return $CHAR_CACHE[$cache_key] = $code;
    }

    /**
     * Parses the string into an array (into the the second parameter).
     *
     * WARNING: Unlike "parse_str()", this method does not (re-)place variables in the current scope,
     *          if the second parameter is not set!
     *
     * EXAMPLE: <code>
     * UTF8::parse_str('Iñtërnâtiônéàlizætiøn=測試&arr[]=foo+測試&arr[]=ການທົດສອບ', $array);
     * echo $array['Iñtërnâtiônéàlizætiøn']; // '測試'
     * </code>
     *
     * @see http://php.net/manual/en/function.parse-str.php
     *
     * @param string $str        <p>The input string.</p>
     * @param array  $result     <p>The result will be returned into this reference parameter.</p>
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Will return <strong>false</strong> if php can't parse the string and we haven't any $result.</p>
     */
    public static function parse_str(string $str, &$result, bool $clean_utf8 = false): bool
    {
        if ($clean_utf8) {
            $str = self::clean($str);
        }

        if (self::$SUPPORT['mbstring'] === true) {
            $return = \mb_parse_str($str, $result);

            return $return !== false && $result !== [];
        }

        /**
         * @psalm-suppress ImpureFunctionCall - we use the second parameter, so we don't change variables by magic
         */
        \parse_str($str, $result);

        return $result !== [];
    }

    /**
     * Checks if \u modifier is available that enables Unicode support in PCRE.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>
     *              <strong>true</strong> if support is available,<br>
     *              <strong>false</strong> otherwise
     *              </p>
     */
    public static function pcre_utf8_support(): bool
    {
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
        return (bool) @\preg_match('//u', '');
    }

    /**
     * Create an array containing a range of UTF-8 characters.
     *
     * EXAMPLE: <code>UTF8::range('κ', 'ζ'); // array('κ', 'ι', 'θ', 'η', 'ζ',)</code>
     *
     * @param int|string $var1      <p>Numeric or hexadecimal code points, or a UTF-8 character to start from.</p>
     * @param int|string $var2      <p>Numeric or hexadecimal code points, or a UTF-8 character to end at.</p>
     * @param bool       $use_ctype <p>use ctype to detect numeric and hexadecimal, otherwise we will use a simple
     *                              "is_numeric"</p>
     * @param string     $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param float|int  $step      [optional] <p>
     *                              If a step value is given, it will be used as the
     *                              increment between elements in the sequence. step
     *                              should be given as a positive number. If not specified,
     *                              step will default to 1.
     *                              </p>
     *
     * @psalm-pure
     *
     * @return string[]
     */
    public static function range(
        $var1,
        $var2,
        bool $use_ctype = true,
        string $encoding = 'UTF-8',
        $step = 1
    ): array {
        if (!$var1 || !$var2) {
            return [];
        }

        if ($step !== 1) {
            /**
             * @psalm-suppress RedundantConditionGivenDocblockType
             * @psalm-suppress DocblockTypeContradiction
             */
            if (!\is_numeric($step)) {
                throw new \InvalidArgumentException('$step need to be a number, type given: ' . \gettype($step));
            }

            /**
             * @psalm-suppress RedundantConditionGivenDocblockType - false-positive from psalm?
             */
            if ($step <= 0) {
                throw new \InvalidArgumentException('$step need to be a positive number, given: ' . $step);
            }
        }

        if ($use_ctype && self::$SUPPORT['ctype'] === false) {
            throw new \RuntimeException('ext-ctype: is not installed');
        }

        $is_digit = false;
        $is_xdigit = false;

        if ($use_ctype && \ctype_digit((string) $var1) && \ctype_digit((string) $var2)) {
            $is_digit = true;
            $start = (int) $var1;
        } elseif ($use_ctype && \ctype_xdigit($var1) && \ctype_xdigit($var2)) {
            $is_xdigit = true;
            $start = (int) self::hex_to_int((string) $var1);
        } elseif (!$use_ctype && \is_numeric($var1)) {
            $start = (int) $var1;
        } else {
            $start = self::ord((string) $var1);
        }

        if (!$start) {
            return [];
        }

        if ($is_digit) {
            $end = (int) $var2;
        } elseif ($is_xdigit) {
            $end = (int) self::hex_to_int((string) $var2);
        } elseif (!$use_ctype && \is_numeric($var2)) {
            $end = (int) $var2;
        } else {
            $end = self::ord((string) $var2);
        }

        if (!$end) {
            return [];
        }

        $array = [];
        foreach (\range($start, $end, $step) as $i) {
            $array[] = (string) self::chr((int) $i, $encoding);
        }

        return $array;
    }

    /**
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
     *
     * EXAMPLE: <code>UTF8::rawurldecode('tes%20öäü%20\u00edtest+test'); // 'tes öäü ítest+test'</code>
     *
     * e.g:
     * 'test+test'                     => 'test+test'
     * 'D&#252;sseldorf'               => 'Düsseldorf'
     * 'D%FCsseldorf'                  => 'Düsseldorf'
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
     * 'Düsseldorf'                   => 'Düsseldorf'
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
     *
     * @param string $str          <p>The input string.</p>
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The decoded URL, as a string.</p>
     */
    public static function rawurldecode(string $str, bool $multi_decode = true): string
    {
        if ($str === '') {
            return '';
        }

        $str = self::urldecode_unicode_helper($str);

        if ($multi_decode) {
            do {
                $str_compare = $str;

                /**
                 * @psalm-suppress PossiblyInvalidArgument
                 */
                $str = \rawurldecode(
                    self::html_entity_decode(
                        self::to_utf8($str),
                        \ENT_QUOTES | \ENT_HTML5
                    )
                );
            } while ($str_compare !== $str);
        } else {
            /**
             * @psalm-suppress PossiblyInvalidArgument
             */
            $str = \rawurldecode(
                self::html_entity_decode(
                    self::to_utf8($str),
                    \ENT_QUOTES | \ENT_HTML5
                )
            );
        }

        return self::fix_simple_utf8($str);
    }

    /**
     * Replaces all occurrences of $pattern in $str by $replacement.
     *
     * @param string $str         <p>The input string.</p>
     * @param string $pattern     <p>The regular expression pattern.</p>
     * @param string $replacement <p>The string to replace with.</p>
     * @param string $options     [optional] <p>Matching conditions to be used.</p>
     * @param string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function regex_replace(
        string $str,
        string $pattern,
        string $replacement,
        string $options = '',
        string $delimiter = '/'
    ): string {
        if ($options === 'msr') {
            $options = 'ms';
        }

        // fallback
        if (!$delimiter) {
            $delimiter = '/';
        }

        return (string) \preg_replace(
            $delimiter . $pattern . $delimiter . 'u' . $options,
            $replacement,
            $str
        );
    }

    /**
     * Remove the BOM from UTF-8 / UTF-16 / UTF-32 strings.
     *
     * EXAMPLE: <code>UTF8::remove_bom("\xEF\xBB\xBFΜπορώ να"); // 'Μπορώ να'</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string without UTF-BOM.</p>
     */
    public static function remove_bom(string $str): string
    {
        if ($str === '') {
            return '';
        }

        $str_length = \strlen($str);
        foreach (self::$BOM as $bom_string => $bom_byte_length) {
            if (\strncmp($str, $bom_string, $bom_byte_length) === 0) {
                /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
                $str_tmp = \substr($str, $bom_byte_length, $str_length);
                if ($str_tmp === false) {
                    return '';
                }

                $str_length -= $bom_byte_length;

                $str = (string) $str_tmp;
            }
        }

        return $str;
    }

    /**
     * Removes duplicate occurrences of a string in another string.
     *
     * EXAMPLE: <code>UTF8::remove_duplicates('öäü-κόσμεκόσμε-äöü', 'κόσμε'); // 'öäü-κόσμε-äöü'</code>
     *
     * @param string          $str  <p>The base string.</p>
     * @param string|string[] $what <p>String to search for in the base string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with removed duplicates.</p>
     */
    public static function remove_duplicates(string $str, $what = ' '): string
    {
        if (\is_string($what)) {
            $what = [$what];
        }

        /**
         * @psalm-suppress RedundantConditionGivenDocblockType
         */
        if (\is_array($what)) {
            foreach ($what as $item) {
                $str = (string) \preg_replace('/(' . \preg_quote($item, '/') . ')+/u', $item, $str);
            }
        }

        return $str;
    }

    /**
     * Remove html via "strip_tags()" from the string.
     *
     * @param string $str            <p>The input string.</p>
     * @param string $allowable_tags [optional] <p>You can use the optional second parameter to specify tags which
     *                               should not be stripped. Default: null
     *                               </p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with without html tags.</p>
     */
    public static function remove_html(string $str, string $allowable_tags = ''): string
    {
        return \strip_tags($str, $allowable_tags);
    }

    /**
     * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
     *
     * @param string $str         <p>The input string.</p>
     * @param string $replacement [optional] <p>Default is a empty string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string without breaks.</p>
     */
    public static function remove_html_breaks(string $str, string $replacement = ''): string
    {
        return (string) \preg_replace("#/\r\n|\r|\n|<br.*/?>#isU", $replacement, $str);
    }

    /**
     * Remove invisible characters from a string.
     *
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
     *
     * EXAMPLE: <code>UTF8::remove_invisible_characters("κόσ\0με"); // 'κόσμε'</code>
     *
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
     *
     * @param string $str                           <p>The input string.</p>
     * @param bool   $url_encoded                   [optional] <p>
     *                                              Try to remove url encoded control character.
     *                                              WARNING: maybe contains false-positives e.g. aa%0Baa -> aaaa.
     *                                              <br>
     *                                              Default: false
     *                                              </p>
     * @param string $replacement                   [optional] <p>The replacement character.</p>
     * @param bool   $keep_basic_control_characters [optional] <p>Keep control characters like [LRM] or [LSEP].</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string without invisible chars.</p>
     */
    public static function remove_invisible_characters(
        string $str,
        bool $url_encoded = false,
        string $replacement = '',
        bool $keep_basic_control_characters = true
    ): string {
        return ASCII::remove_invisible_characters(
            $str,
            $url_encoded,
            $replacement,
            $keep_basic_control_characters
        );
    }

    /**
     * Returns a new string with the prefix $substring removed, if present.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $substring <p>The prefix to remove.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string without the prefix $substring.</p>
     */
    public static function remove_left(
        string $str,
        string $substring,
        string $encoding = 'UTF-8'
    ): string {
        if (
            $substring
            &&
            \strpos($str, $substring) === 0
        ) {
            if ($encoding === 'UTF-8') {
                return (string) \mb_substr(
                    $str,
                    (int) \mb_strlen($substring)
                );
            }

            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            return (string) self::substr(
                $str,
                (int) self::strlen($substring, $encoding),
                null,
                $encoding
            );
        }

        return $str;
    }

    /**
     * Returns a new string with the suffix $substring removed, if present.
     *
     * @param string $str
     * @param string $substring <p>The suffix to remove.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string having a $str without the suffix $substring.</p>
     */
    public static function remove_right(
        string $str,
        string $substring,
        string $encoding = 'UTF-8'
    ): string {
        if ($substring && \substr($str, -\strlen($substring)) === $substring) {
            if ($encoding === 'UTF-8') {
                return (string) \mb_substr(
                    $str,
                    0,
                    (int) \mb_strlen($str) - (int) \mb_strlen($substring)
                );
            }

            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            return (string) self::substr(
                $str,
                0,
                (int) self::strlen($str, $encoding) - (int) self::strlen($substring, $encoding),
                $encoding
            );
        }

        return $str;
    }

    /**
     * Replaces all occurrences of $search in $str by $replacement.
     *
     * @param string $str            <p>The input string.</p>
     * @param string $search         <p>The needle to search for.</p>
     * @param string $replacement    <p>The string to replace with.</p>
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with replaced parts.</p>
     */
    public static function replace(
        string $str,
        string $search,
        string $replacement,
        bool $case_sensitive = true
    ): string {
        if ($case_sensitive) {
            return \str_replace($search, $replacement, $str);
        }

        return self::str_ireplace($search, $replacement, $str);
    }

    /**
     * Replaces all occurrences of $search in $str by $replacement.
     *
     * @param string       $str            <p>The input string.</p>
     * @param array        $search         <p>The elements to search for.</p>
     * @param array|string $replacement    <p>The string to replace with.</p>
     * @param bool         $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with replaced parts.</p>
     */
    public static function replace_all(
        string $str,
        array $search,
        $replacement,
        bool $case_sensitive = true
    ): string {
        if ($case_sensitive) {
            return \str_replace($search, $replacement, $str);
        }

        return self::str_ireplace($search, $replacement, $str);
    }

    /**
     * Replace the diamond question mark (�) and invalid-UTF8 chars with the replacement.
     *
     * EXAMPLE: <code>UTF8::replace_diamond_question_mark('中文空白�', ''); // '中文空白'</code>
     *
     * @param string $str                        <p>The input string</p>
     * @param string $replacement_char           <p>The replacement character.</p>
     * @param bool   $process_invalid_utf8_chars <p>Convert invalid UTF-8 chars </p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string without diamond question marks (�).</p>
     */
    public static function replace_diamond_question_mark(
        string $str,
        string $replacement_char = '',
        bool $process_invalid_utf8_chars = true
    ): string {
        if ($str === '') {
            return '';
        }

        if ($process_invalid_utf8_chars) {
            if ($replacement_char === '') {
                $replacement_char_helper = 'none';
            } else {
                $replacement_char_helper = \ord($replacement_char);
            }

            if (self::$SUPPORT['mbstring'] === false) {
                // if there is no native support for "mbstring",
                // then we need to clean the string before ...
                $str = self::clean($str);
            }

            /**
             * @psalm-suppress ImpureFunctionCall - we will reset the value in the next step
             */
            $save = \mb_substitute_character();
            /** @noinspection PhpUsageOfSilenceOperatorInspection - ignore "Unknown character" warnings, it's working anyway */
            @\mb_substitute_character($replacement_char_helper);
            // the polyfill maybe return false, so cast to string
            $str = (string) \mb_convert_encoding($str, 'UTF-8', 'UTF-8');
            \mb_substitute_character($save);
        }

        return \str_replace(
            [
                "\xEF\xBF\xBD",
                '�',
            ],
            [
                $replacement_char,
                $replacement_char,
            ],
            $str
        );
    }

    /**
     * Strip whitespace or other characters from the end of a UTF-8 string.
     *
     * EXAMPLE: <code>UTF8::rtrim('-ABC-中文空白-  '); // '-ABC-中文空白-'</code>
     *
     * @param string      $str   <p>The string to be trimmed.</p>
     * @param string|null $chars <p>Optional characters to be stripped.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with unwanted characters stripped from the right.</p>
     */
    public static function rtrim(string $str = '', string $chars = null): string
    {
        if ($str === '') {
            return '';
        }

        if (self::$SUPPORT['mbstring'] === true) {
            if ($chars !== null) {
                /** @noinspection PregQuoteUsageInspection */
                $chars = \preg_quote($chars);
                $pattern = "[${chars}]+$";
            } else {
                $pattern = '[\\s]+$';
            }

            return (string) \mb_ereg_replace($pattern, '', $str);
        }

        if ($chars !== null) {
            $chars = \preg_quote($chars, '/');
            $pattern = "[${chars}]+$";
        } else {
            $pattern = '[\\s]+$';
        }

        return self::regex_replace($str, $pattern, '');
    }

    /**
     * WARNING: Print native UTF-8 support (libs) by default, e.g. for debugging.
     *
     * @param bool $useEcho
     *
     * @psalm-pure
     *
     * @return string|void
     */
    public static function showSupport(bool $useEcho = true)
    {
        // init
        $html = '';

        $html .= '<pre>';
        foreach (self::$SUPPORT as $key => &$value) {
            $html .= $key . ' - ' . \print_r($value, true) . "\n<br>";
        }
        $html .= '</pre>';

        if ($useEcho) {
            echo $html;
        }

        return $html;
    }

    /**
     * Converts a UTF-8 character to HTML Numbered Entity like "&#123;".
     *
     * EXAMPLE: <code>UTF8::single_chr_html_encode('κ'); // '&#954;'</code>
     *
     * @param string $char             <p>The Unicode character to be encoded as numbered entity.</p>
     * @param bool   $keep_ascii_chars <p>Set to <strong>true</strong> to keep ASCII chars.</>
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The HTML numbered entity for the given character.</p>
     */
    public static function single_chr_html_encode(
        string $char,
        bool $keep_ascii_chars = false,
        string $encoding = 'UTF-8'
    ): string {
        if ($char === '') {
            return '';
        }

        if (
            $keep_ascii_chars
            &&
            ASCII::is_ascii($char)
        ) {
            return $char;
        }

        return '&#' . self::ord($char, $encoding) . ';';
    }

    /**
     * @param string $str
     * @param int    $tab_length
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function spaces_to_tabs(string $str, int $tab_length = 4): string
    {
        if ($tab_length === 4) {
            $tab = '    ';
        } elseif ($tab_length === 2) {
            $tab = '  ';
        } else {
            $tab = \str_repeat(' ', $tab_length);
        }

        return \str_replace($tab, "\t", $str);
    }

    /**
     * Returns a camelCase version of the string. Trims surrounding spaces,
     * capitalizes letters following digits, spaces, dashes and underscores,
     * and removes spaces, dashes, as well as underscores.
     *
     * @param string      $str                           <p>The input string.</p>
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_camelize(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        if ($clean_utf8) {
            $str = self::clean($str);
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        $str = self::lcfirst(
            \trim($str),
            $encoding,
            false,
            $lang,
            $try_to_keep_the_string_length
        );
        $str = (string) \preg_replace('/^[-_]+/', '', $str);

        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;

        $str = (string) \preg_replace_callback(
            '/[-_\\s]+(.)?/u',
            /**
             * @param array $match
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $match) use ($use_mb_functions, $encoding, $lang, $try_to_keep_the_string_length): string {
                if (isset($match[1])) {
                    if ($use_mb_functions) {
                        if ($encoding === 'UTF-8') {
                            return \mb_strtoupper($match[1]);
                        }

                        return \mb_strtoupper($match[1], $encoding);
                    }

                    return self::strtoupper($match[1], $encoding, false, $lang, $try_to_keep_the_string_length);
                }

                return '';
            },
            $str
        );

        return (string) \preg_replace_callback(
            '/[\\p{N}]+(.)?/u',
            /**
             * @param array $match
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $match) use ($use_mb_functions, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length): string {
                if ($use_mb_functions) {
                    if ($encoding === 'UTF-8') {
                        return \mb_strtoupper($match[0]);
                    }

                    return \mb_strtoupper($match[0], $encoding);
                }

                return self::strtoupper($match[0], $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
            },
            $str
        );
    }

    /**
     * Returns the string with the first letter of each word capitalized,
     * except for when the word is a name which shouldn't be capitalized.
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with $str capitalized.</p>
     */
    public static function str_capitalize_name(string $str): string
    {
        return self::str_capitalize_name_helper(
            self::str_capitalize_name_helper(
                self::collapse_whitespace($str),
                ' '
            ),
            '-'
        );
    }

    /**
     * Returns true if the string contains $needle, false otherwise. By default
     * the comparison is case-sensitive, but can be made insensitive by setting
     * $case_sensitive to false.
     *
     * @param string $haystack       <p>The input string.</p>
     * @param string $needle         <p>Substring to look for.</p>
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $haystack contains $needle.</p>
     */
    public static function str_contains(
        string $haystack,
        string $needle,
        bool $case_sensitive = true
    ): bool {
        if ($case_sensitive) {
            if (\PHP_VERSION_ID >= 80000) {
                /** @phpstan-ignore-next-line - only for PHP8 */
                return \str_contains($haystack, $needle);
            }

            return \strpos($haystack, $needle) !== false;
        }

        return \mb_stripos($haystack, $needle) !== false;
    }

    /**
     * Returns true if the string contains all $needles, false otherwise. By
     * default the comparison is case-sensitive, but can be made insensitive by
     * setting $case_sensitive to false.
     *
     * @param string $haystack       <p>The input string.</p>
     * @param array  $needles        <p>SubStrings to look for.</p>
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $haystack contains $needle.</p>
     */
    public static function str_contains_all(
        string $haystack,
        array $needles,
        bool $case_sensitive = true
    ): bool {
        if ($haystack === '' || $needles === []) {
            return false;
        }

        foreach ($needles as &$needle) {
            if ($case_sensitive) {
                if (!$needle || \strpos($haystack, $needle) === false) {
                    return false;
                }
            }

            if (!$needle || \mb_stripos($haystack, $needle) === false) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns true if the string contains any $needles, false otherwise. By
     * default the comparison is case-sensitive, but can be made insensitive by
     * setting $case_sensitive to false.
     *
     * @param string $haystack       <p>The input string.</p>
     * @param array  $needles        <p>SubStrings to look for.</p>
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str contains $needle.</p>
     */
    public static function str_contains_any(
        string $haystack,
        array $needles,
        bool $case_sensitive = true
    ): bool {
        if ($haystack === '' || $needles === []) {
            return false;
        }

        foreach ($needles as &$needle) {
            if (!$needle) {
                continue;
            }

            if ($case_sensitive) {
                if (\strpos($haystack, $needle) !== false) {
                    return true;
                }

                continue;
            }

            if (\mb_stripos($haystack, $needle) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns a lowercase and trimmed string separated by dashes. Dashes are
     * inserted before uppercase characters (with the exception of the first
     * character of the string), and in place of spaces as well as underscores.
     *
     * @param string $str      <p>The input string.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_dasherize(string $str, string $encoding = 'UTF-8'): string
    {
        return self::str_delimit($str, '-', $encoding);
    }

    /**
     * Returns a lowercase and trimmed string separated by the given delimiter.
     * Delimiters are inserted before uppercase characters (with the exception
     * of the first character of the string), and in place of spaces, dashes,
     * and underscores. Alpha delimiters are not converted to lowercase.
     *
     * @param string      $str                           <p>The input string.</p>
     * @param string      $delimiter                     <p>Sequence used to separate parts of the string.</p>
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
     *                                                   ß</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_delimit(
        string $str,
        string $delimiter,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        if (self::$SUPPORT['mbstring'] === true) {
            $str = (string) \mb_ereg_replace('\\B(\\p{Lu})', '-\1', \trim($str));

            $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
            if ($use_mb_functions && $encoding === 'UTF-8') {
                $str = \mb_strtolower($str);
            } else {
                $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
            }

            return (string) \mb_ereg_replace('[\\-_\\s]+', $delimiter, $str);
        }

        $str = (string) \preg_replace('/\\B(\\p{Lu})/u', '-\1', \trim($str));

        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
        if ($use_mb_functions && $encoding === 'UTF-8') {
            $str = \mb_strtolower($str);
        } else {
            $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
        }

        return (string) \preg_replace('/[\\-_\\s]+/u', $delimiter, $str);
    }

    /**
     * Optimized "mb_detect_encoding()"-function -> with support for UTF-16 and UTF-32.
     *
     * EXAMPLE: <code>
     * UTF8::str_detect_encoding('中文空白'); // 'UTF-8'
     * UTF8::str_detect_encoding('Abc'); // 'ASCII'
     * </code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>
     *                      The detected string-encoding e.g. UTF-8 or UTF-16BE,<br>
     *                      otherwise it will return false e.g. for BINARY or not detected encoding.
     *                      </p>
     */
    public static function str_detect_encoding($str)
    {
        // init
        $str = (string) $str;

        //
        // 1.) check binary strings (010001001...) like UTF-16 / UTF-32 / PDF / Images / ...
        //

        if (self::is_binary($str, self::string_has_bom($str) ? false : true)) {
            $is_utf32 = self::is_utf32($str, false);
            if ($is_utf32 === 1) {
                return 'UTF-32LE';
            }
            if ($is_utf32 === 2) {
                return 'UTF-32BE';
            }

            $is_utf16 = self::is_utf16($str, false);
            if ($is_utf16 === 1) {
                return 'UTF-16LE';
            }
            if ($is_utf16 === 2) {
                return 'UTF-16BE';
            }

            // is binary but not "UTF-16" or "UTF-32"
            return false;
        }

        //
        // 2.) simple check for ASCII chars
        //

        if (ASCII::is_ascii($str)) {
            return 'ASCII';
        }

        //
        // 3.) simple check for UTF-8 chars
        //

        if (self::is_utf8_string($str)) {
            return 'UTF-8';
        }

        //
        // 4.) check via "mb_detect_encoding()"
        //
        // INFO: UTF-16, UTF-32, UCS2 and UCS4, encoding detection will fail always with "mb_detect_encoding()"

        $encoding_detecting_order = [
            'ISO-8859-1',
            'ISO-8859-2',
            'ISO-8859-3',
            'ISO-8859-4',
            'ISO-8859-5',
            'ISO-8859-6',
            'ISO-8859-7',
            'ISO-8859-8',
            'ISO-8859-9',
            'ISO-8859-10',
            'ISO-8859-13',
            'ISO-8859-14',
            'ISO-8859-15',
            'ISO-8859-16',
            'WINDOWS-1251',
            'WINDOWS-1252',
            'WINDOWS-1254',
            'CP932',
            'CP936',
            'CP950',
            'CP866',
            'CP850',
            'CP51932',
            'CP50220',
            'CP50221',
            'CP50222',
            'ISO-2022-JP',
            'ISO-2022-KR',
            'JIS',
            'JIS-ms',
            'EUC-CN',
            'EUC-JP',
        ];

        if (self::$SUPPORT['mbstring'] === true) {
            // info: do not use the symfony polyfill here
            $encoding = \mb_detect_encoding($str, $encoding_detecting_order, true);
            if ($encoding) {
                return $encoding;
            }
        }

        //
        // 5.) check via "iconv()"
        //

        if (self::$ENCODINGS === null) {
            self::$ENCODINGS = self::getData('encodings');
        }

        foreach (self::$ENCODINGS as $encoding_tmp) {
            // INFO: //IGNORE but still throw notice
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
            if ((string) @\iconv($encoding_tmp, $encoding_tmp . '//IGNORE', $str) === $str) {
                return $encoding_tmp;
            }
        }

        return false;
    }

    /**
     * Check if the string ends with the given substring.
     *
     * EXAMPLE: <code>
     * UTF8::str_ends_with('BeginMiddleΚόσμε', 'Κόσμε'); // true
     * UTF8::str_ends_with('BeginMiddleΚόσμε', 'κόσμε'); // false
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function str_ends_with(string $haystack, string $needle): bool
    {
        if ($needle === '') {
            return true;
        }

        if ($haystack === '') {
            return false;
        }

        if (\PHP_VERSION_ID >= 80000) {
            /** @phpstan-ignore-next-line - only for PHP8 */
            return \str_ends_with($haystack, $needle);
        }

        return \substr($haystack, -\strlen($needle)) === $needle;
    }

    /**
     * Returns true if the string ends with any of $substrings, false otherwise.
     *
     * - case-sensitive
     *
     * @param string   $str        <p>The input string.</p>
     * @param string[] $substrings <p>Substrings to look for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str ends with $substring.</p>
     */
    public static function str_ends_with_any(string $str, array $substrings): bool
    {
        if ($substrings === []) {
            return false;
        }

        foreach ($substrings as &$substring) {
            if (\substr($str, -\strlen($substring)) === $substring) {
                return true;
            }
        }

        return false;
    }

    /**
     * Ensures that the string begins with $substring. If it doesn't, it's
     * prepended.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $substring <p>The substring to add if not present.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_ensure_left(string $str, string $substring): string
    {
        if (
            $substring !== ''
            &&
            \strpos($str, $substring) === 0
        ) {
            return $str;
        }

        return $substring . $str;
    }

    /**
     * Ensures that the string ends with $substring. If it doesn't, it's appended.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $substring <p>The substring to add if not present.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_ensure_right(string $str, string $substring): string
    {
        if (
            $str === ''
            ||
            $substring === ''
            ||
            \substr($str, -\strlen($substring)) !== $substring
        ) {
            $str .= $substring;
        }

        return $str;
    }

    /**
     * Capitalizes the first word of the string, replaces underscores with
     * spaces, and strips '_id'.
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_humanize($str): string
    {
        $str = \str_replace(
            [
                '_id',
                '_',
            ],
            [
                '',
                ' ',
            ],
            $str
        );

        return self::ucfirst(\trim($str));
    }

    /**
     * Check if the string ends with the given substring, case-insensitive.
     *
     * EXAMPLE: <code>
     * UTF8::str_iends_with('BeginMiddleΚόσμε', 'Κόσμε'); // true
     * UTF8::str_iends_with('BeginMiddleΚόσμε', 'κόσμε'); // true
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function str_iends_with(string $haystack, string $needle): bool
    {
        if ($needle === '') {
            return true;
        }

        if ($haystack === '') {
            return false;
        }

        return self::strcasecmp(\substr($haystack, -\strlen($needle)), $needle) === 0;
    }

    /**
     * Returns true if the string ends with any of $substrings, false otherwise.
     *
     * - case-insensitive
     *
     * @param string   $str        <p>The input string.</p>
     * @param string[] $substrings <p>Substrings to look for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str ends with $substring.</p>
     */
    public static function str_iends_with_any(string $str, array $substrings): bool
    {
        if ($substrings === []) {
            return false;
        }

        foreach ($substrings as &$substring) {
            if (self::str_iends_with($str, $substring)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Inserts $substring into the string at the $index provided.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $substring <p>String to be inserted.</p>
     * @param int    $index     <p>The index at which to insert the substring.</p>
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_insert(
        string $str,
        string $substring,
        int $index,
        string $encoding = 'UTF-8'
    ): string {
        if ($encoding === 'UTF-8') {
            $len = (int) \mb_strlen($str);
            if ($index > $len) {
                return $str;
            }

            /** @noinspection UnnecessaryCastingInspection */
            return (string) \mb_substr($str, 0, $index) .
                   $substring .
                   (string) \mb_substr($str, $index, $len);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        $len = (int) self::strlen($str, $encoding);
        if ($index > $len) {
            return $str;
        }

        return ((string) self::substr($str, 0, $index, $encoding)) .
               $substring .
               ((string) self::substr($str, $index, $len, $encoding));
    }

    /**
     * Case-insensitive and UTF-8 safe version of <function>str_replace</function>.
     *
     * EXAMPLE: <code>
     * UTF8::str_ireplace('lIzÆ', 'lise', 'Iñtërnâtiônàlizætiøn'); // 'Iñtërnâtiônàlisetiøn'
     * </code>
     *
     * @see http://php.net/manual/en/function.str-ireplace.php
     *
     * @param string|string[] $search      <p>
     *                                     Every replacement with search array is
     *                                     performed on the result of previous replacement.
     *                                     </p>
     * @param string|string[] $replacement <p>The replacement.</p>
     * @param string|string[] $subject     <p>
     *                                     If subject is an array, then the search and
     *                                     replace is performed with every entry of
     *                                     subject, and the return value is an array as
     *                                     well.
     *                                     </p>
     * @param int             $count       [optional] <p>
     *                                     The number of matched and replaced needles will
     *                                     be returned in count which is passed by
     *                                     reference.
     *                                     </p>
     *
     * @psalm-pure
     *
     * @return string|string[]
     *                         <p>A string or an array of replacements.</p>
     *
     * @template TStrIReplaceSubject
     * @phpstan-param TStrIReplaceSubject $subject
     * @phpstan-return TStrIReplaceSubject
     */
    public static function str_ireplace($search, $replacement, $subject, &$count = null)
    {
        $search = (array) $search;

        /** @noinspection AlterInForeachInspection */
        foreach ($search as &$s) {
            $s = (string) $s;
            if ($s === '') {
                $s = '/^(?<=.)$/';
            } else {
                $s = '/' . \preg_quote($s, '/') . '/ui';
            }
        }

        // fallback
        /** @phpstan-ignore-next-line - only a fallback for PHP8 */
        if ($replacement === null) {
            $replacement = '';
        }
        /** @phpstan-ignore-next-line - only a fallback for PHP8 */
        if ($subject === null) {
            $subject = '';
        }

        /**
         * @psalm-suppress PossiblyNullArgument
         * @phpstan-var TStrIReplaceSubject $subject
         */
        $subject = \preg_replace($search, $replacement, $subject, -1, $count);

        return $subject;
    }

    /**
     * Replaces $search from the beginning of string with $replacement.
     *
     * @param string $str         <p>The input string.</p>
     * @param string $search      <p>The string to search for.</p>
     * @param string $replacement <p>The replacement.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The string after the replacement.</p>
     */
    public static function str_ireplace_beginning(string $str, string $search, string $replacement): string
    {
        if ($str === '') {
            if ($replacement === '') {
                return '';
            }

            if ($search === '') {
                return $replacement;
            }
        }

        if ($search === '') {
            return $str . $replacement;
        }

        $searchLength = \strlen($search);
        if (\strncasecmp($str, $search, $searchLength) === 0) {
            return $replacement . \substr($str, $searchLength);
        }

        return $str;
    }

    /**
     * Replaces $search from the ending of string with $replacement.
     *
     * @param string $str         <p>The input string.</p>
     * @param string $search      <p>The string to search for.</p>
     * @param string $replacement <p>The replacement.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The string after the replacement.</p>
     */
    public static function str_ireplace_ending(string $str, string $search, string $replacement): string
    {
        if ($str === '') {
            if ($replacement === '') {
                return '';
            }

            if ($search === '') {
                return $replacement;
            }
        }

        if ($search === '') {
            return $str . $replacement;
        }

        if (\stripos($str, $search, \strlen($str) - \strlen($search)) !== false) {
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
        }

        return $str;
    }

    /**
     * Check if the string starts with the given substring, case-insensitive.
     *
     * EXAMPLE: <code>
     * UTF8::str_istarts_with('ΚόσμεMiddleEnd', 'Κόσμε'); // true
     * UTF8::str_istarts_with('ΚόσμεMiddleEnd', 'κόσμε'); // true
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function str_istarts_with(string $haystack, string $needle): bool
    {
        if ($needle === '') {
            return true;
        }

        if ($haystack === '') {
            return false;
        }

        return self::stripos($haystack, $needle) === 0;
    }

    /**
     * Returns true if the string begins with any of $substrings, false otherwise.
     *
     * - case-insensitive
     *
     * @param string $str        <p>The input string.</p>
     * @param array  $substrings <p>Substrings to look for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str starts with $substring.</p>
     */
    public static function str_istarts_with_any(string $str, array $substrings): bool
    {
        if ($str === '') {
            return false;
        }

        if ($substrings === []) {
            return false;
        }

        foreach ($substrings as &$substring) {
            if (self::str_istarts_with($str, $substring)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets the substring after the first occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_isubstr_after_first_separator(
        string $str,
        string $separator,
        string $encoding = 'UTF-8'
    ): string {
        if ($separator === '' || $str === '') {
            return '';
        }

        $offset = self::stripos($str, $separator);
        if ($offset === false) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr(
                $str,
                $offset + (int) \mb_strlen($separator)
            );
        }

        return (string) self::substr(
            $str,
            $offset + (int) self::strlen($separator, $encoding),
            null,
            $encoding
        );
    }

    /**
     * Gets the substring after the last occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_isubstr_after_last_separator(
        string $str,
        string $separator,
        string $encoding = 'UTF-8'
    ): string {
        if ($separator === '' || $str === '') {
            return '';
        }

        $offset = self::strripos($str, $separator);
        if ($offset === false) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr(
                $str,
                $offset + (int) self::strlen($separator)
            );
        }

        return (string) self::substr(
            $str,
            $offset + (int) self::strlen($separator, $encoding),
            null,
            $encoding
        );
    }

    /**
     * Gets the substring before the first occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_isubstr_before_first_separator(
        string $str,
        string $separator,
        string $encoding = 'UTF-8'
    ): string {
        if ($separator === '' || $str === '') {
            return '';
        }

        $offset = self::stripos($str, $separator);
        if ($offset === false) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr($str, 0, $offset);
        }

        return (string) self::substr($str, 0, $offset, $encoding);
    }

    /**
     * Gets the substring before the last occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_isubstr_before_last_separator(
        string $str,
        string $separator,
        string $encoding = 'UTF-8'
    ): string {
        if ($separator === '' || $str === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            $offset = \mb_strripos($str, $separator);
            if ($offset === false) {
                return '';
            }

            return (string) \mb_substr($str, 0, $offset);
        }

        $offset = self::strripos($str, $separator, 0, $encoding);
        if ($offset === false) {
            return '';
        }

        return (string) self::substr($str, 0, $offset, $encoding);
    }

    /**
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
     *
     * @param string $str           <p>The input string.</p>
     * @param string $needle        <p>The string to look for.</p>
     * @param bool   $before_needle [optional] <p>Default: false</p>
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_isubstr_first(
        string $str,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8'
    ): string {
        if (
            $needle === ''
            ||
            $str === ''
        ) {
            return '';
        }

        $part = self::stristr(
            $str,
            $needle,
            $before_needle,
            $encoding
        );
        if ($part === false) {
            return '';
        }

        return $part;
    }

    /**
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
     *
     * @param string $str           <p>The input string.</p>
     * @param string $needle        <p>The string to look for.</p>
     * @param bool   $before_needle [optional] <p>Default: false</p>
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_isubstr_last(
        string $str,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8'
    ): string {
        if (
            $needle === ''
            ||
            $str === ''
        ) {
            return '';
        }

        $part = self::strrichr(
            $str,
            $needle,
            $before_needle,
            $encoding
        );
        if ($part === false) {
            return '';
        }

        return $part;
    }

    /**
     * Returns the last $n characters of the string.
     *
     * @param string $str      <p>The input string.</p>
     * @param int    $n        <p>Number of characters to retrieve from the end.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_last_char(
        string $str,
        int $n = 1,
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '' || $n <= 0) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr($str, -$n);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        return (string) self::substr($str, -$n, null, $encoding);
    }

    /**
     * Limit the number of characters in a string.
     *
     * @param string $str        <p>The input string.</p>
     * @param int    $length     [optional] <p>Default: 100</p>
     * @param string $str_add_on [optional] <p>Default: …</p>
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_limit(
        string $str,
        int $length = 100,
        string $str_add_on = '…',
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '' || $length <= 0) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            if ((int) \mb_strlen($str) <= $length) {
                return $str;
            }

            /** @noinspection UnnecessaryCastingInspection */
            return (string) \mb_substr($str, 0, $length - (int) self::strlen($str_add_on)) . $str_add_on;
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if ((int) self::strlen($str, $encoding) <= $length) {
            return $str;
        }

        return ((string) self::substr($str, 0, $length - (int) self::strlen($str_add_on), $encoding)) . $str_add_on;
    }

    /**
     * Limit the number of characters in a string, but also after the next word.
     *
     * EXAMPLE: <code>UTF8::str_limit_after_word('fòô bàř fòô', 8, ''); // 'fòô bàř'</code>
     *
     * @param string $str        <p>The input string.</p>
     * @param int    $length     [optional] <p>Default: 100</p>
     * @param string $str_add_on [optional] <p>Default: …</p>
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_limit_after_word(
        string $str,
        int $length = 100,
        string $str_add_on = '…',
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '' || $length <= 0) {
            return '';
        }

        if ($encoding === 'UTF-8') {
            if ((int) \mb_strlen($str) <= $length) {
                return $str;
            }

            if (\mb_substr($str, $length - 1, 1) === ' ') {
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
            }

            $str = \mb_substr($str, 0, $length);

            $array = \explode(' ', $str, -1);
            $new_str = \implode(' ', $array);

            if ($new_str === '') {
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
            }
        } else {
            if ((int) self::strlen($str, $encoding) <= $length) {
                return $str;
            }

            if (self::substr($str, $length - 1, 1, $encoding) === ' ') {
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
            }

            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
            $str = self::substr($str, 0, $length, $encoding);
            if ($str === false) {
                return '' . $str_add_on;
            }

            $array = \explode(' ', $str, -1);
            $new_str = \implode(' ', $array);

            if ($new_str === '') {
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
            }
        }

        return $new_str . $str_add_on;
    }

    /**
     * Returns the longest common prefix between the $str1 and $str2.
     *
     * @param string $str1     <p>The input sting.</p>
     * @param string $str2     <p>Second string for comparison.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_longest_common_prefix(
        string $str1,
        string $str2,
        string $encoding = 'UTF-8'
    ): string {
        // init
        $longest_common_prefix = '';

        if ($encoding === 'UTF-8') {
            $max_length = (int) \min(
                \mb_strlen($str1),
                \mb_strlen($str2)
            );

            for ($i = 0; $i < $max_length; ++$i) {
                $char = \mb_substr($str1, $i, 1);

                if (
                    $char !== false
                    &&
                    $char === \mb_substr($str2, $i, 1)
                ) {
                    $longest_common_prefix .= $char;
                } else {
                    break;
                }
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $max_length = (int) \min(
                self::strlen($str1, $encoding),
                self::strlen($str2, $encoding)
            );

            for ($i = 0; $i < $max_length; ++$i) {
                $char = self::substr($str1, $i, 1, $encoding);

                if (
                    $char !== false
                    &&
                    $char === self::substr($str2, $i, 1, $encoding)
                ) {
                    $longest_common_prefix .= $char;
                } else {
                    break;
                }
            }
        }

        return $longest_common_prefix;
    }

    /**
     * Returns the longest common substring between the $str1 and $str2.
     * In the case of ties, it returns that which occurs first.
     *
     * @param string $str1
     * @param string $str2     <p>Second string for comparison.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with its $str being the longest common substring.</p>
     */
    public static function str_longest_common_substring(
        string $str1,
        string $str2,
        string $encoding = 'UTF-8'
    ): string {
        if ($str1 === '' || $str2 === '') {
            return '';
        }

        // Uses dynamic programming to solve
        // http://en.wikipedia.org/wiki/Longest_common_substring_problem

        if ($encoding === 'UTF-8') {
            $str_length = (int) \mb_strlen($str1);
            $other_length = (int) \mb_strlen($str2);
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $str_length = (int) self::strlen($str1, $encoding);
            $other_length = (int) self::strlen($str2, $encoding);
        }

        // Return if either string is empty
        if ($str_length === 0 || $other_length === 0) {
            return '';
        }

        $len = 0;
        $end = 0;
        $table = \array_fill(
            0,
            $str_length + 1,
            \array_fill(0, $other_length + 1, 0)
        );

        if ($encoding === 'UTF-8') {
            for ($i = 1; $i <= $str_length; ++$i) {
                for ($j = 1; $j <= $other_length; ++$j) {
                    $str_char = \mb_substr($str1, $i - 1, 1);
                    $other_char = \mb_substr($str2, $j - 1, 1);

                    if ($str_char === $other_char) {
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
                        if ($table[$i][$j] > $len) {
                            $len = $table[$i][$j];
                            $end = $i;
                        }
                    } else {
                        $table[$i][$j] = 0;
                    }
                }
            }
        } else {
            for ($i = 1; $i <= $str_length; ++$i) {
                for ($j = 1; $j <= $other_length; ++$j) {
                    $str_char = self::substr($str1, $i - 1, 1, $encoding);
                    $other_char = self::substr($str2, $j - 1, 1, $encoding);

                    if ($str_char === $other_char) {
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
                        if ($table[$i][$j] > $len) {
                            $len = $table[$i][$j];
                            $end = $i;
                        }
                    } else {
                        $table[$i][$j] = 0;
                    }
                }
            }
        }

        if ($encoding === 'UTF-8') {
            return (string) \mb_substr($str1, $end - $len, $len);
        }

        return (string) self::substr($str1, $end - $len, $len, $encoding);
    }

    /**
     * Returns the longest common suffix between the $str1 and $str2.
     *
     * @param string $str1
     * @param string $str2     <p>Second string for comparison.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_longest_common_suffix(
        string $str1,
        string $str2,
        string $encoding = 'UTF-8'
    ): string {
        if ($str1 === '' || $str2 === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            $max_length = (int) \min(
                \mb_strlen($str1, $encoding),
                \mb_strlen($str2, $encoding)
            );

            $longest_common_suffix = '';
            for ($i = 1; $i <= $max_length; ++$i) {
                $char = \mb_substr($str1, -$i, 1);

                if (
                    $char !== false
                    &&
                    $char === \mb_substr($str2, -$i, 1)
                ) {
                    $longest_common_suffix = $char . $longest_common_suffix;
                } else {
                    break;
                }
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $max_length = (int) \min(
                self::strlen($str1, $encoding),
                self::strlen($str2, $encoding)
            );

            $longest_common_suffix = '';
            for ($i = 1; $i <= $max_length; ++$i) {
                $char = self::substr($str1, -$i, 1, $encoding);

                if (
                    $char !== false
                    &&
                    $char === self::substr($str2, -$i, 1, $encoding)
                ) {
                    $longest_common_suffix = $char . $longest_common_suffix;
                } else {
                    break;
                }
            }
        }

        return $longest_common_suffix;
    }

    /**
     * Returns true if $str matches the supplied pattern, false otherwise.
     *
     * @param string $str     <p>The input string.</p>
     * @param string $pattern <p>Regex pattern to match against.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str matches the pattern.</p>
     */
    public static function str_matches_pattern(string $str, string $pattern): bool
    {
        return (bool) \preg_match('/' . $pattern . '/u', $str);
    }

    /**
     * Returns whether or not a character exists at an index. Offsets may be
     * negative to count from the last character in the string. Implements
     * part of the ArrayAccess interface.
     *
     * @param string $str      <p>The input string.</p>
     * @param int    $offset   <p>The index to check.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not the index exists.</p>
     */
    public static function str_offset_exists(string $str, int $offset, string $encoding = 'UTF-8'): bool
    {
        // init
        $length = (int) self::strlen($str, $encoding);

        if ($offset >= 0) {
            return $length > $offset;
        }

        return $length >= \abs($offset);
    }

    /**
     * Returns the character at the given index. Offsets may be negative to
     * count from the last character in the string. Implements part of the
     * ArrayAccess interface, and throws an OutOfBoundsException if the index
     * does not exist.
     *
     * @param string $str      <p>The input string.</p>
     * @param int    $index    <p>The <strong>index</strong> from which to retrieve the char.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @throws \OutOfBoundsException if the positive or negative offset does not exist
     *
     * @return string
     *                <p>The character at the specified index.</p>
     *
     * @psalm-pure
     */
    public static function str_offset_get(string $str, int $index, string $encoding = 'UTF-8'): string
    {
        // init
        $length = (int) self::strlen($str);

        if (
            ($index >= 0 && $length <= $index)
            ||
            $length < \abs($index)
        ) {
            throw new \OutOfBoundsException('No character exists at the index');
        }

        return self::char_at($str, $index, $encoding);
    }

    /**
     * Pad a UTF-8 string to a given length with another string.
     *
     * EXAMPLE: <code>UTF8::str_pad('中文空白', 10, '_', STR_PAD_BOTH); // '___中文空白___'</code>
     *
     * @param string     $str        <p>The input string.</p>
     * @param int        $pad_length <p>The length of return string.</p>
     * @param string     $pad_string [optional] <p>String to use for padding the input string.</p>
     * @param int|string $pad_type   [optional] <p>
     *                               Can be <strong>STR_PAD_RIGHT</strong> (default), [or string "right"]<br>
     *                               <strong>STR_PAD_LEFT</strong> [or string "left"] or<br>
     *                               <strong>STR_PAD_BOTH</strong> [or string "both"]
     *                               </p>
     * @param string     $encoding   [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Returns the padded string.</p>
     */
    public static function str_pad(
        string $str,
        int $pad_length,
        string $pad_string = ' ',
        $pad_type = \STR_PAD_RIGHT,
        string $encoding = 'UTF-8'
    ): string {
        if ($pad_length === 0 || $pad_string === '') {
            return $str;
        }

        if ($pad_type !== (int) $pad_type) {
            if ($pad_type === 'left') {
                $pad_type = \STR_PAD_LEFT;
            } elseif ($pad_type === 'right') {
                $pad_type = \STR_PAD_RIGHT;
            } elseif ($pad_type === 'both') {
                $pad_type = \STR_PAD_BOTH;
            } else {
                throw new \InvalidArgumentException(
                    'Pad expects $pad_type to be "STR_PAD_*" or ' . "to be one of 'left', 'right' or 'both'"
                );
            }
        }

        if ($encoding === 'UTF-8') {
            $str_length = (int) \mb_strlen($str);

            if ($pad_length >= $str_length) {
                switch ($pad_type) {
                    case \STR_PAD_LEFT:
                        $ps_length = (int) \mb_strlen($pad_string);

                        $diff = ($pad_length - $str_length);

                        $pre = (string) \mb_substr(
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
                            0,
                            $diff
                        );
                        $post = '';

                        break;

                    case \STR_PAD_BOTH:
                        $diff = ($pad_length - $str_length);

                        $ps_length_left = (int) \floor($diff / 2);

                        $ps_length_right = (int) \ceil($diff / 2);

                        $pre = (string) \mb_substr(
                            \str_repeat($pad_string, $ps_length_left),
                            0,
                            $ps_length_left
                        );
                        $post = (string) \mb_substr(
                            \str_repeat($pad_string, $ps_length_right),
                            0,
                            $ps_length_right
                        );

                        break;

                    case \STR_PAD_RIGHT:
                    default:
                        $ps_length = (int) \mb_strlen($pad_string);

                        $diff = ($pad_length - $str_length);

                        $post = (string) \mb_substr(
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
                            0,
                            $diff
                        );
                        $pre = '';
                }

                return $pre . $str . $post;
            }

            return $str;
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        $str_length = (int) self::strlen($str, $encoding);

        if ($pad_length >= $str_length) {
            switch ($pad_type) {
                case \STR_PAD_LEFT:
                    $ps_length = (int) self::strlen($pad_string, $encoding);

                    $diff = ($pad_length - $str_length);

                    $pre = (string) self::substr(
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
                        0,
                        $diff,
                        $encoding
                    );
                    $post = '';

                    break;

                case \STR_PAD_BOTH:
                    $diff = ($pad_length - $str_length);

                    $ps_length_left = (int) \floor($diff / 2);

                    $ps_length_right = (int) \ceil($diff / 2);

                    $pre = (string) self::substr(
                        \str_repeat($pad_string, $ps_length_left),
                        0,
                        $ps_length_left,
                        $encoding
                    );
                    $post = (string) self::substr(
                        \str_repeat($pad_string, $ps_length_right),
                        0,
                        $ps_length_right,
                        $encoding
                    );

                    break;

                case \STR_PAD_RIGHT:
                default:
                    $ps_length = (int) self::strlen($pad_string, $encoding);

                    $diff = ($pad_length - $str_length);

                    $post = (string) self::substr(
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
                        0,
                        $diff,
                        $encoding
                    );
                    $pre = '';
            }

            return $pre . $str . $post;
        }

        return $str;
    }

    /**
     * Returns a new string of a given length such that both sides of the
     * string are padded. Alias for "UTF8::str_pad()" with a $pad_type of 'both'.
     *
     * @param string $str
     * @param int    $length   <p>Desired string length after padding.</p>
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The string with padding applied.</p>
     */
    public static function str_pad_both(
        string $str,
        int $length,
        string $pad_str = ' ',
        string $encoding = 'UTF-8'
    ): string {
        return self::str_pad(
            $str,
            $length,
            $pad_str,
            \STR_PAD_BOTH,
            $encoding
        );
    }

    /**
     * Returns a new string of a given length such that the beginning of the
     * string is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'left'.
     *
     * @param string $str
     * @param int    $length   <p>Desired string length after padding.</p>
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The string with left padding.</p>
     */
    public static function str_pad_left(
        string $str,
        int $length,
        string $pad_str = ' ',
        string $encoding = 'UTF-8'
    ): string {
        return self::str_pad(
            $str,
            $length,
            $pad_str,
            \STR_PAD_LEFT,
            $encoding
        );
    }

    /**
     * Returns a new string of a given length such that the end of the string
     * is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'right'.
     *
     * @param string $str
     * @param int    $length   <p>Desired string length after padding.</p>
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The string with right padding.</p>
     */
    public static function str_pad_right(
        string $str,
        int $length,
        string $pad_str = ' ',
        string $encoding = 'UTF-8'
    ): string {
        return self::str_pad(
            $str,
            $length,
            $pad_str,
            \STR_PAD_RIGHT,
            $encoding
        );
    }

    /**
     * Repeat a string.
     *
     * EXAMPLE: <code>UTF8::str_repeat("°~\xf0\x90\x28\xbc", 2); // '°~ð(¼°~ð(¼'</code>
     *
     * @param string $str        <p>
     *                           The string to be repeated.
     *                           </p>
     * @param int    $multiplier <p>
     *                           Number of time the input string should be
     *                           repeated.
     *                           </p>
     *                           <p>
     *                           multiplier has to be greater than or equal to 0.
     *                           If the multiplier is set to 0, the function
     *                           will return an empty string.
     *                           </p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The repeated string.</p>
     */
    public static function str_repeat(string $str, int $multiplier): string
    {
        $str = self::filter($str);

        return \str_repeat($str, $multiplier);
    }

    /**
     * INFO: This is only a wrapper for "str_replace()"  -> the original functions is already UTF-8 safe.
     *
     * Replace all occurrences of the search string with the replacement string
     *
     * @see http://php.net/manual/en/function.str-replace.php
     *
     * @param string|string[] $search  <p>
     *                                 The value being searched for, otherwise known as the needle.
     *                                 An array may be used to designate multiple needles.
     *                                 </p>
     * @param string|string[] $replace <p>
     *                                 The replacement value that replaces found search
     *                                 values. An array may be used to designate multiple replacements.
     *                                 </p>
     * @param string|string[] $subject <p>
     *                                 The string or array of strings being searched and replaced on,
     *                                 otherwise known as the haystack.
     *                                 </p>
     *                                 <p>
     *                                 If subject is an array, then the search and
     *                                 replace is performed with every entry of
     *                                 subject, and the return value is an array as
     *                                 well.
     *                                 </p>
     * @param int|null        $count   [optional] <p>
     *                                 If passed, this will hold the number of matched and replaced needles.
     *                                 </p>
     *
     * @psalm-pure
     *
     * @return string|string[]
     *                         <p>This function returns a string or an array with the replaced values.</p>
     *
     * @template TStrReplaceSubject
     * @phpstan-param TStrReplaceSubject $subject
     * @phpstan-return TStrReplaceSubject
     *
     * @deprecated please use \str_replace() instead
     */
    public static function str_replace(
        $search,
        $replace,
        $subject,
        int &$count = null
    ) {
        /**
         * @psalm-suppress PossiblyNullArgument
         * @phpstan-var TStrReplaceSubject $return;
         */
        $return = \str_replace(
            $search,
            $replace,
            $subject,
            $count
        );

        return $return;
    }

    /**
     * Replaces $search from the beginning of string with $replacement.
     *
     * @param string $str         <p>The input string.</p>
     * @param string $search      <p>The string to search for.</p>
     * @param string $replacement <p>The replacement.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string after the replacements.</p>
     */
    public static function str_replace_beginning(
        string $str,
        string $search,
        string $replacement
    ): string {
        if ($str === '') {
            if ($replacement === '') {
                return '';
            }

            if ($search === '') {
                return $replacement;
            }
        }

        if ($search === '') {
            return $str . $replacement;
        }

        $searchLength = \strlen($search);
        if (\strncmp($str, $search, $searchLength) === 0) {
            return $replacement . \substr($str, $searchLength);
        }

        return $str;
    }

    /**
     * Replaces $search from the ending of string with $replacement.
     *
     * @param string $str         <p>The input string.</p>
     * @param string $search      <p>The string to search for.</p>
     * @param string $replacement <p>The replacement.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string after the replacements.</p>
     */
    public static function str_replace_ending(
        string $str,
        string $search,
        string $replacement
    ): string {
        if ($str === '') {
            if ($replacement === '') {
                return '';
            }

            if ($search === '') {
                return $replacement;
            }
        }

        if ($search === '') {
            return $str . $replacement;
        }

        if (\strpos($str, $search, \strlen($str) - \strlen($search)) !== false) {
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
        }

        return $str;
    }

    /**
     * Replace the first "$search"-term with the "$replace"-term.
     *
     * @param string $search
     * @param string $replace
     * @param string $subject
     *
     * @psalm-pure
     *
     * @return string
     *
     * @psalm-suppress InvalidReturnType
     */
    public static function str_replace_first(
        string $search,
        string $replace,
        string $subject
    ): string {
        $pos = self::strpos($subject, $search);

        if ($pos !== false) {
            /**
             * @psalm-suppress InvalidReturnStatement
             */
            return self::substr_replace(
                $subject,
                $replace,
                $pos,
                (int) self::strlen($search)
            );
        }

        return $subject;
    }

    /**
     * Replace the last "$search"-term with the "$replace"-term.
     *
     * @param string $search
     * @param string $replace
     * @param string $subject
     *
     * @psalm-pure
     *
     * @return string
     *
     * @psalm-suppress InvalidReturnType
     */
    public static function str_replace_last(
        string $search,
        string $replace,
        string $subject
    ): string {
        $pos = self::strrpos($subject, $search);
        if ($pos !== false) {
            /**
             * @psalm-suppress InvalidReturnStatement
             */
            return self::substr_replace(
                $subject,
                $replace,
                $pos,
                (int) self::strlen($search)
            );
        }

        return $subject;
    }

    /**
     * Shuffles all the characters in the string.
     *
     * INFO: uses random algorithm which is weak for cryptography purposes
     *
     * EXAMPLE: <code>UTF8::str_shuffle('fòô bàř fòô'); // 'àòôřb ffòô '</code>
     *
     * @param string $str      <p>The input string</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @return string
     *                <p>The shuffled string.</p>
     */
    public static function str_shuffle(string $str, string $encoding = 'UTF-8'): string
    {
        if ($encoding === 'UTF-8') {
            $indexes = \range(0, (int) \mb_strlen($str) - 1);
            \shuffle($indexes);

            // init
            $shuffled_str = '';

            foreach ($indexes as &$i) {
                $tmp_sub_str = \mb_substr($str, $i, 1);
                if ($tmp_sub_str !== false) {
                    $shuffled_str .= $tmp_sub_str;
                }
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $indexes = \range(0, (int) self::strlen($str, $encoding) - 1);
            \shuffle($indexes);

            // init
            $shuffled_str = '';

            foreach ($indexes as &$i) {
                $tmp_sub_str = self::substr($str, $i, 1, $encoding);
                if ($tmp_sub_str !== false) {
                    $shuffled_str .= $tmp_sub_str;
                }
            }
        }

        return $shuffled_str;
    }

    /**
     * Returns the substring beginning at $start, and up to, but not including
     * the index specified by $end. If $end is omitted, the function extracts
     * the remaining string. If $end is negative, it is computed from the end
     * of the string.
     *
     * @param string   $str
     * @param int      $start    <p>Initial index from which to begin extraction.</p>
     * @param int|null $end      [optional] <p>Index at which to end extraction. Default: null</p>
     * @param string   $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>The extracted substring.</p><p>If <i>str</i> is shorter than <i>start</i>
     *                      characters long, <b>FALSE</b> will be returned.
     */
    public static function str_slice(
        string $str,
        int $start,
        int $end = null,
        string $encoding = 'UTF-8'
    ) {
        if ($encoding === 'UTF-8') {
            if ($end === null) {
                $length = (int) \mb_strlen($str);
            } elseif ($end >= 0 && $end <= $start) {
                return '';
            } elseif ($end < 0) {
                $length = (int) \mb_strlen($str) + $end - $start;
            } else {
                $length = $end - $start;
            }

            return \mb_substr($str, $start, $length);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if ($end === null) {
            $length = (int) self::strlen($str, $encoding);
        } elseif ($end >= 0 && $end <= $start) {
            return '';
        } elseif ($end < 0) {
            $length = (int) self::strlen($str, $encoding) + $end - $start;
        } else {
            $length = $end - $start;
        }

        return self::substr($str, $start, $length, $encoding);
    }

    /**
     * Convert a string to e.g.: "snake_case"
     *
     * @param string $str
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string in snake_case.</p>
     */
    public static function str_snakeize(string $str, string $encoding = 'UTF-8'): string
    {
        if ($str === '') {
            return '';
        }

        $str = \str_replace(
            '-',
            '_',
            self::normalize_whitespace($str)
        );

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        $str = (string) \preg_replace_callback(
            '/([\\p{N}|\\p{Lu}])/u',
            /**
             * @param string[] $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches) use ($encoding): string {
                $match = $matches[1];
                $match_int = (int) $match;

                if ((string) $match_int === $match) {
                    return '_' . $match . '_';
                }

                if ($encoding === 'UTF-8') {
                    return '_' . \mb_strtolower($match);
                }

                return '_' . self::strtolower($match, $encoding);
            },
            $str
        );

        $str = (string) \preg_replace(
            [
                '/\\s+/u',           // convert spaces to "_"
                '/^\\s+|\\s+$/u', // trim leading & trailing spaces
                '/_+/',                 // remove double "_"
            ],
            [
                '_',
                '',
                '_',
            ],
            $str
        );

        return \trim(\trim($str, '_')); // trim leading & trailing "_" + whitespace
    }

    /**
     * Sort all characters according to code points.
     *
     * EXAMPLE: <code>UTF8::str_sort('  -ABC-中文空白-  '); // '    ---ABC中文白空'</code>
     *
     * @param string $str    <p>A UTF-8 string.</p>
     * @param bool   $unique <p>Sort unique. If <strong>true</strong>, repeated characters are ignored.</p>
     * @param bool   $desc   <p>If <strong>true</strong>, will sort characters in reverse code point order.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string of sorted characters.</p>
     */
    public static function str_sort(string $str, bool $unique = false, bool $desc = false): string
    {
        /** @var int[] $array */
        $array = self::codepoints($str);

        if ($unique) {
            $array = \array_flip(\array_flip($array));
        }

        if ($desc) {
            \arsort($array);
        } else {
            \asort($array);
        }

        return self::string($array);
    }

    /**
     * Convert a string to an array of Unicode characters.
     *
     * EXAMPLE: <code>
     * UTF8::str_split_array(['中文空白', 'test'], 2); // [['中文', '空白'], ['te', 'st']]
     * </code>
     *
     * @param int[]|string[] $input                   <p>The string[] or int[] to split into array.</p>
     * @param int            $length                  [optional] <p>Max character length of each array
     *                                                lement.</p>
     * @param bool           $clean_utf8              [optional] <p>Remove non UTF-8 chars from the
     *                                                string.</p>
     * @param bool           $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
     *                                                "mb_substr"</p>
     *
     * @psalm-pure
     *
     * @return string[][]
     *                    <p>An array containing chunks of the input.</p>
     */
    public static function str_split_array(
        array $input,
        int $length = 1,
        bool $clean_utf8 = false,
        bool $try_to_use_mb_functions = true
    ): array {
        foreach ($input as &$v) {
            $v = self::str_split(
                $v,
                $length,
                $clean_utf8,
                $try_to_use_mb_functions
            );
        }

        /** @var string[][] $input */
        return $input;
    }

    /**
     * Convert a string to an array of unicode characters.
     *
     * EXAMPLE: <code>UTF8::str_split('中文空白'); // array('中', '文', '空', '白')</code>
     *
     * @param int|string $input                   <p>The string or int to split into array.</p>
     * @param int        $length                  [optional] <p>Max character length of each array
     *                                            element.</p>
     * @param bool       $clean_utf8              [optional] <p>Remove non UTF-8 chars from the
     *                                            string.</p>
     * @param bool       $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
     *                                            "mb_substr"</p>
     *
     * @psalm-pure
     *
     * @return string[]
     *                  <p>An array containing chunks of chars from the input.</p>
     */
    public static function str_split(
        $input,
        int $length = 1,
        bool $clean_utf8 = false,
        bool $try_to_use_mb_functions = true
    ): array {
        if ($length <= 0) {
            return [];
        }

        // this is only an old fallback
        /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
        /** @var int|int[]|string|string[] $input */
        $input = $input;
        if (\is_array($input)) {
            /** @psalm-suppress InvalidReturnStatement */
            /** @phpstan-ignore-next-line - old code :/ */
            return self::str_split_array(
                $input,
                $length,
                $clean_utf8,
                $try_to_use_mb_functions
            );
        }

        // init
        $input = (string) $input;

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

        if ($clean_utf8) {
            $input = self::clean($input);
        }

        if (
            $try_to_use_mb_functions
            &&
            self::$SUPPORT['mbstring'] === true
        ) {
            if (\function_exists('mb_str_split')) {
                /**
                 * @psalm-suppress ImpureFunctionCall - why?
                 */
                $return = \mb_str_split($input, $length);
                if ($return !== false) {
                    return $return;
                }
            }

            $i_max = \mb_strlen($input);
            if ($i_max <= 127) {
                $ret = [];
                for ($i = 0; $i < $i_max; ++$i) {
                    $ret[] = \mb_substr($input, $i, 1);
                }
            } else {
                $return_array = [];
                \preg_match_all('/./us', $input, $return_array);
                $ret = $return_array[0] ?? [];
            }
        } elseif (self::$SUPPORT['pcre_utf8'] === true) {
            $return_array = [];
            \preg_match_all('/./us', $input, $return_array);
            $ret = $return_array[0] ?? [];
        } else {

            // fallback

            $ret = [];
            $len = \strlen($input);

            for ($i = 0; $i < $len; ++$i) {
                if (($input[$i] & "\x80") === "\x00") {
                    $ret[] = $input[$i];
                } elseif (
                    isset($input[$i + 1])
                    &&
                    ($input[$i] & "\xE0") === "\xC0"
                ) {
                    if (($input[$i + 1] & "\xC0") === "\x80") {
                        $ret[] = $input[$i] . $input[$i + 1];

                        ++$i;
                    }
                } elseif (
                    isset($input[$i + 2])
                    &&
                    ($input[$i] & "\xF0") === "\xE0"
                ) {
                    if (
                        ($input[$i + 1] & "\xC0") === "\x80"
                        &&
                        ($input[$i + 2] & "\xC0") === "\x80"
                    ) {
                        $ret[] = $input[$i] . $input[$i + 1] . $input[$i + 2];

                        $i += 2;
                    }
                } elseif (
                    isset($input[$i + 3])
                    &&
                    ($input[$i] & "\xF8") === "\xF0"
                ) {
                    if (
                        ($input[$i + 1] & "\xC0") === "\x80"
                        &&
                        ($input[$i + 2] & "\xC0") === "\x80"
                        &&
                        ($input[$i + 3] & "\xC0") === "\x80"
                    ) {
                        $ret[] = $input[$i] . $input[$i + 1] . $input[$i + 2] . $input[$i + 3];

                        $i += 3;
                    }
                }
            }
        }

        if ($length > 1) {
            return \array_map(
                static function (array $item): string {
                    return \implode('', $item);
                },
                \array_chunk($ret, $length)
            );
        }

        if (isset($ret[0]) && $ret[0] === '') {
            return [];
        }

        return $ret;
    }

    /**
     * Splits the string with the provided regular expression, returning an
     * array of strings. An optional integer $limit will truncate the
     * results.
     *
     * @param string $str
     * @param string $pattern <p>The regex with which to split the string.</p>
     * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
     *
     * @psalm-pure
     *
     * @return string[]
     *                  <p>An array of strings.</p>
     */
    public static function str_split_pattern(string $str, string $pattern, int $limit = -1): array
    {
        if ($limit === 0) {
            return [];
        }

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

        if (self::$SUPPORT['mbstring'] === true) {
            if ($limit >= 0) {
                $result_tmp = \mb_split($pattern, $str);
                if ($result_tmp === false) {
                    return [];
                }

                $result = [];
                foreach ($result_tmp as $item_tmp) {
                    if ($limit === 0) {
                        break;
                    }
                    --$limit;

                    $result[] = $item_tmp;
                }

                return $result;
            }

            $result = \mb_split($pattern, $str);
            if ($result === false) {
                return [];
            }

            return $result;
        }

        if ($limit > 0) {
            ++$limit;
        } else {
            $limit = -1;
        }

        $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $str, $limit);
        if ($array === false) {
            return [];
        }

        if ($limit > 0 && \count($array) === $limit) {
            \array_pop($array);
        }

        return $array;
    }

    /**
     * Check if the string starts with the given substring.
     *
     * EXAMPLE: <code>
     * UTF8::str_starts_with('ΚόσμεMiddleEnd', 'Κόσμε'); // true
     * UTF8::str_starts_with('ΚόσμεMiddleEnd', 'κόσμε'); // false
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function str_starts_with(string $haystack, string $needle): bool
    {
        if ($needle === '') {
            return true;
        }

        if ($haystack === '') {
            return false;
        }

        if (\PHP_VERSION_ID >= 80000) {
            /** @phpstan-ignore-next-line - only for PHP8 */
            return \str_starts_with($haystack, $needle);
        }

        return \strncmp($haystack, $needle, \strlen($needle)) === 0;
    }

    /**
     * Returns true if the string begins with any of $substrings, false otherwise.
     *
     * - case-sensitive
     *
     * @param string $str        <p>The input string.</p>
     * @param array  $substrings <p>Substrings to look for.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>Whether or not $str starts with $substring.</p>
     */
    public static function str_starts_with_any(string $str, array $substrings): bool
    {
        if ($str === '') {
            return false;
        }

        if ($substrings === []) {
            return false;
        }

        foreach ($substrings as &$substring) {
            if (self::str_starts_with($str, $substring)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets the substring after the first occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_substr_after_first_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
    {
        if ($separator === '' || $str === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            $offset = \mb_strpos($str, $separator);
            if ($offset === false) {
                return '';
            }

            return (string) \mb_substr(
                $str,
                $offset + (int) \mb_strlen($separator)
            );
        }

        $offset = self::strpos($str, $separator, 0, $encoding);
        if ($offset === false) {
            return '';
        }

        return (string) \mb_substr(
            $str,
            $offset + (int) self::strlen($separator, $encoding),
            null,
            $encoding
        );
    }

    /**
     * Gets the substring after the last occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_substr_after_last_separator(
        string $str,
        string $separator,
        string $encoding = 'UTF-8'
    ): string {
        if ($separator === '' || $str === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            $offset = \mb_strrpos($str, $separator);
            if ($offset === false) {
                return '';
            }

            return (string) \mb_substr(
                $str,
                $offset + (int) \mb_strlen($separator)
            );
        }

        $offset = self::strrpos($str, $separator, 0, $encoding);
        if ($offset === false) {
            return '';
        }

        return (string) self::substr(
            $str,
            $offset + (int) self::strlen($separator, $encoding),
            null,
            $encoding
        );
    }

    /**
     * Gets the substring before the first occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_substr_before_first_separator(
        string $str,
        string $separator,
        string $encoding = 'UTF-8'
    ): string {
        if ($separator === '' || $str === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            $offset = \mb_strpos($str, $separator);
            if ($offset === false) {
                return '';
            }

            return (string) \mb_substr(
                $str,
                0,
                $offset
            );
        }

        $offset = self::strpos($str, $separator, 0, $encoding);
        if ($offset === false) {
            return '';
        }

        return (string) self::substr(
            $str,
            0,
            $offset,
            $encoding
        );
    }

    /**
     * Gets the substring before the last occurrence of a separator.
     *
     * @param string $str       <p>The input string.</p>
     * @param string $separator <p>The string separator.</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_substr_before_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
    {
        if ($separator === '' || $str === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            $offset = \mb_strrpos($str, $separator);
            if ($offset === false) {
                return '';
            }

            return (string) \mb_substr(
                $str,
                0,
                $offset
            );
        }

        $offset = self::strrpos($str, $separator, 0, $encoding);
        if ($offset === false) {
            return '';
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        return (string) self::substr(
            $str,
            0,
            $offset,
            $encoding
        );
    }

    /**
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
     *
     * @param string $str           <p>The input string.</p>
     * @param string $needle        <p>The string to look for.</p>
     * @param bool   $before_needle [optional] <p>Default: false</p>
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_substr_first(
        string $str,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '' || $needle === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            if ($before_needle) {
                $part = \mb_strstr(
                    $str,
                    $needle,
                    $before_needle
                );
            } else {
                $part = \mb_strstr(
                    $str,
                    $needle
                );
            }
        } else {
            $part = self::strstr(
                $str,
                $needle,
                $before_needle,
                $encoding
            );
        }

        return $part === false ? '' : $part;
    }

    /**
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
     *
     * @param string $str           <p>The input string.</p>
     * @param string $needle        <p>The string to look for.</p>
     * @param bool   $before_needle [optional] <p>Default: false</p>
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function str_substr_last(
        string $str,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '' || $needle === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            if ($before_needle) {
                $part = \mb_strrchr(
                    $str,
                    $needle,
                    $before_needle
                );
            } else {
                $part = \mb_strrchr(
                    $str,
                    $needle
                );
            }
        } else {
            $part = self::strrchr(
                $str,
                $needle,
                $before_needle,
                $encoding
            );
        }

        return $part === false ? '' : $part;
    }

    /**
     * Surrounds $str with the given substring.
     *
     * @param string $str
     * @param string $substring <p>The substring to add to both sides.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with the substring both prepended and appended.</p>
     */
    public static function str_surround(string $str, string $substring): string
    {
        return $substring . $str . $substring;
    }

    /**
     * Returns a trimmed string with the first letter of each word capitalized.
     * Also accepts an array, $ignore, allowing you to list words not to be
     * capitalized.
     *
     * @param string              $str
     * @param array|string[]|null $ignore                        [optional] <p>An array of words not to capitalize or
     *                                                           null. Default: null</p>
     * @param string              $encoding                      [optional] <p>Default: 'UTF-8'</p>
     * @param bool                $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the
     *                                                           string.</p>
     * @param string|null         $lang                          [optional] <p>Set the language for special cases: az,
     *                                                           el, lt, tr</p>
     * @param bool                $try_to_keep_the_string_length [optional] <p>true === try to keep the string length:
     *                                                           e.g. ẞ -> ß</p>
     * @param bool                $use_trim_first                [optional] <p>true === trim the input string,
     *                                                           first</p>
     * @param string|null         $word_define_chars             [optional] <p>An string of chars that will be used as
     *                                                           whitespace separator === words.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The titleized string.</p>
     */
    public static function str_titleize(
        string $str,
        array $ignore = null,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false,
        bool $use_trim_first = true,
        string $word_define_chars = null
    ): string {
        if ($str === '') {
            return '';
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($use_trim_first) {
            $str = \trim($str);
        }

        if ($clean_utf8) {
            $str = self::clean($str);
        }

        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;

        if ($word_define_chars) {
            $word_define_chars = \preg_quote($word_define_chars, '/');
        } else {
            $word_define_chars = '';
        }

        $str = (string) \preg_replace_callback(
            '/([^\\s' . $word_define_chars . ']+)/u',
            static function (array $match) use ($try_to_keep_the_string_length, $lang, $ignore, $use_mb_functions, $encoding): string {
                if ($ignore !== null && \in_array($match[0], $ignore, true)) {
                    return $match[0];
                }

                if ($use_mb_functions) {
                    if ($encoding === 'UTF-8') {
                        return \mb_strtoupper(\mb_substr($match[0], 0, 1))
                               . \mb_strtolower(\mb_substr($match[0], 1));
                    }

                    return \mb_strtoupper(\mb_substr($match[0], 0, 1, $encoding), $encoding)
                           . \mb_strtolower(\mb_substr($match[0], 1, null, $encoding), $encoding);
                }

                return self::ucfirst(
                    self::strtolower(
                        $match[0],
                        $encoding,
                        false,
                        $lang,
                        $try_to_keep_the_string_length
                    ),
                    $encoding,
                    false,
                    $lang,
                    $try_to_keep_the_string_length
                );
            },
            $str
        );

        return $str;
    }

    /**
     * Convert a string into a obfuscate string.
     *
     * EXAMPLE: <code>
     *
     * UTF8::str_obfuscate('lars@moelleken.org', 0.5, '*', ['@', '.']); // e.g. "l***@m**lleke*.*r*"
     * </code>
     *
     * @param string   $str
     * @param float    $percent
     * @param string   $obfuscateChar
     * @param string[] $keepChars
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The obfuscate string.</p>
     */
    public static function str_obfuscate(
        string $str,
        float $percent = 0.5,
        string $obfuscateChar = '*',
        array $keepChars = []
    ): string {
        $obfuscateCharHelper = "\u{2603}";
        $str = \str_replace($obfuscateChar, $obfuscateCharHelper, $str);

        $chars = self::chars($str);
        $charsMax = \count($chars);
        $charsMaxChange = \round($charsMax * $percent);
        $charsCounter = 0;
        $charKeyDone = [];

        while ($charsCounter < $charsMaxChange) {
            foreach ($chars as $charKey => $char) {
                if (isset($charKeyDone[$charKey])) {
                    continue;
                }

                if (\random_int(0, 100) > 50) {
                    continue;
                }

                if ($char === $obfuscateChar) {
                    continue;
                }

                ++$charsCounter;
                $charKeyDone[$charKey] = true;

                if ($charsCounter > $charsMaxChange) {
                    break;
                }

                if (\in_array($char, $keepChars, true)) {
                    continue;
                }

                $chars[$charKey] = $obfuscateChar;
            }
        }

        $str = \implode('', $chars);

        return \str_replace($obfuscateCharHelper, $obfuscateChar, $str);
    }

    /**
     * Returns a trimmed string in proper title case.
     *
     * Also accepts an array, $ignore, allowing you to list words not to be
     * capitalized.
     *
     * Adapted from John Gruber's script.
     *
     * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
     *
     * @param string $str
     * @param array  $ignore   <p>An array of words not to capitalize.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The titleized string.</p>
     */
    public static function str_titleize_for_humans(
        string $str,
        array $ignore = [],
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '') {
            return '';
        }

        $small_words = [
            '(?<!q&)a',
            'an',
            'and',
            'as',
            'at(?!&t)',
            'but',
            'by',
            'en',
            'for',
            'if',
            'in',
            'of',
            'on',
            'or',
            'the',
            'to',
            'v[.]?',
            'via',
            'vs[.]?',
        ];

        if ($ignore !== []) {
            $small_words = \array_merge($small_words, $ignore);
        }

        $small_words_rx = \implode('|', $small_words);
        $apostrophe_rx = '(?x: [\'’] [[:lower:]]* )?';

        $str = \trim($str);

        if (!self::has_lowercase($str)) {
            $str = self::strtolower($str, $encoding);
        }

        // the main substitutions
        $str = (string) \preg_replace_callback(
            '~\\b (_*) (?:                                                           # 1. Leading underscore and
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |                # 2. file path or 
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostrophe_rx . ' )  #    URL, domain, or email
                        |
                        ( (?i: ' . $small_words_rx . ' ) ' . $apostrophe_rx . ' )           # 3. or small word (case-insensitive)
                        |
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 4. or word w/o internal caps
                        |
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 5. or some other word
                      ) (_*) \\b                                                          # 6. With trailing underscore
                    ~ux',
            /**
             * @param string[] $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches) use ($encoding): string {
                // preserve leading underscore
                $str = $matches[1];
                if ($matches[2]) {
                    // preserve URLs, domains, emails and file paths
                    $str .= $matches[2];
                } elseif ($matches[3]) {
                    // lower-case small words
                    $str .= self::strtolower($matches[3], $encoding);
                } elseif ($matches[4]) {
                    // capitalize word w/o internal caps
                    $str .= static::ucfirst($matches[4], $encoding);
                } else {
                    // preserve other kinds of word (iPhone)
                    $str .= $matches[5];
                }
                // preserve trailing underscore
                $str .= $matches[6];

                return $str;
            },
            $str
        );

        // Exceptions for small words: capitalize at start of title...
        $str = (string) \preg_replace_callback(
            '~(  \\A [[:punct:]]*            # start of title...
                      |  [:.;?!][ ]+                # or of subsentence...
                      |  [ ][\'"“‘(\[][ ]* )        # or of inserted subphrase...
                      ( ' . $small_words_rx . ' ) \\b # ...followed by small word
                     ~uxi',
            /**
             * @param string[] $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches) use ($encoding): string {
                return $matches[1] . static::ucfirst($matches[2], $encoding);
            },
            $str
        );

        // ...and end of title
        $str = (string) \preg_replace_callback(
            '~\\b ( ' . $small_words_rx . ' ) # small word...
                      (?= [[:punct:]]* \Z          # ...at the end of the title...
                      |   [\'"’”)\]] [ ] )         # ...or of an inserted subphrase?
                     ~uxi',
            /**
             * @param string[] $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches) use ($encoding): string {
                return static::ucfirst($matches[1], $encoding);
            },
            $str
        );

        // Exceptions for small words in hyphenated compound words.
        // e.g. "in-flight" -> In-Flight
        $str = (string) \preg_replace_callback(
            '~\\b
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
                        ( ' . $small_words_rx . ' )
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
                       ~uxi',
            /**
             * @param string[] $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches) use ($encoding): string {
                return static::ucfirst($matches[1], $encoding);
            },
            $str
        );

        // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
        $str = (string) \preg_replace_callback(
            '~\\b
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
                      ( ' . $small_words_rx . ' ) # ...followed by small word
                      (?!	- )                 # Negative lookahead for another -
                     ~uxi',
            /**
             * @param string[] $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches) use ($encoding): string {
                return $matches[1] . static::ucfirst($matches[2], $encoding);
            },
            $str
        );

        return $str;
    }

    /**
     * Get a binary representation of a specific string.
     *
     * EXAPLE: <code>UTF8::str_to_binary('😃'); // '11110000100111111001100010000011'</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>false on error</p>
     */
    public static function str_to_binary(string $str)
    {
        /** @var array|false $value - needed for PhpStan (stubs error) */
        $value = \unpack('H*', $str);
        if ($value === false) {
            return false;
        }

        /** @noinspection OffsetOperationsInspection */
        return \base_convert($value[1], 16, 2);
    }

    /**
     * @param string   $str
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
     *
     * @psalm-pure
     *
     * @return string[]
     */
    public static function str_to_lines(string $str, bool $remove_empty_values = false, int $remove_short_values = null): array
    {
        if ($str === '') {
            return $remove_empty_values ? [] : [''];
        }

        if (self::$SUPPORT['mbstring'] === true) {
            $return = \mb_split("[\r\n]{1,2}", $str);
        } else {
            $return = \preg_split("/[\r\n]{1,2}/u", $str);
        }

        if ($return === false) {
            return $remove_empty_values ? [] : [''];
        }

        if (
            $remove_short_values === null
            &&
            !$remove_empty_values
        ) {
            return $return;
        }

        return self::reduce_string_array(
            $return,
            $remove_empty_values,
            $remove_short_values
        );
    }

    /**
     * Convert a string into an array of words.
     *
     * EXAMPLE: <code>UTF8::str_to_words('中文空白 oöäü#s', '#') // array('', '中文空白', ' ', 'oöäü#s', '')</code>
     *
     * @param string   $str
     * @param string   $char_list           <p>Additional chars for the definition of "words".</p>
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
     *
     * @psalm-pure
     *
     * @return string[]
     */
    public static function str_to_words(
        string $str,
        string $char_list = '',
        bool $remove_empty_values = false,
        int $remove_short_values = null
    ): array {
        if ($str === '') {
            return $remove_empty_values ? [] : [''];
        }

        $char_list = self::rxClass($char_list, '\pL');

        $return = \preg_split("/({$char_list}+(?:[\p{Pd}’']{$char_list}+)*)/u", $str, -1, \PREG_SPLIT_DELIM_CAPTURE);
        if ($return === false) {
            return $remove_empty_values ? [] : [''];
        }

        if (
            $remove_short_values === null
            &&
            !$remove_empty_values
        ) {
            return $return;
        }

        $tmp_return = self::reduce_string_array(
            $return,
            $remove_empty_values,
            $remove_short_values
        );

        foreach ($tmp_return as &$item) {
            $item = (string) $item;
        }

        return $tmp_return;
    }

    /**
     * Truncates the string to a given length. If $substring is provided, and
     * truncating occurs, the string is further truncated so that the substring
     * may be appended without exceeding the desired length.
     *
     * @param string $str
     * @param int    $length    <p>Desired length of the truncated string.</p>
     * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string after truncating.</p>
     */
    public static function str_truncate(
        string $str,
        int $length,
        string $substring = '',
        string $encoding = 'UTF-8'
    ): string {
        if ($str === '') {
            return '';
        }

        if ($encoding === 'UTF-8') {
            if ($length >= (int) \mb_strlen($str)) {
                return $str;
            }

            if ($substring !== '') {
                $length -= (int) \mb_strlen($substring);

                /** @noinspection UnnecessaryCastingInspection */
                return (string) \mb_substr($str, 0, $length) . $substring;
            }

            return (string) \mb_substr($str, 0, $length);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if ($length >= (int) self::strlen($str, $encoding)) {
            return $str;
        }

        if ($substring !== '') {
            $length -= (int) self::strlen($substring, $encoding);
        }

        return (
               (string) self::substr(
                   $str,
                   0,
                   $length,
                   $encoding
               )
               ) . $substring;
    }

    /**
     * Truncates the string to a given length, while ensuring that it does not
     * split words. If $substring is provided, and truncating occurs, the
     * string is further truncated so that the substring may be appended without
     * exceeding the desired length.
     *
     * @param string $str
     * @param int    $length                                 <p>Desired length of the truncated string.</p>
     * @param string $substring                              [optional] <p>The substring to append if it can fit.
     *                                                       Default:
     *                                                       ''</p>
     * @param string $encoding                               [optional] <p>Default: 'UTF-8'</p>
     * @param bool   $ignore_do_not_split_words_for_one_word [optional] <p>Default: false</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string after truncating.</p>
     */
    public static function str_truncate_safe(
        string $str,
        int $length,
        string $substring = '',
        string $encoding = 'UTF-8',
        bool $ignore_do_not_split_words_for_one_word = false
    ): string {
        if ($str === '' || $length <= 0) {
            return $substring;
        }

        if ($encoding === 'UTF-8') {
            if ($length >= (int) \mb_strlen($str)) {
                return $str;
            }

            // need to further trim the string so we can append the substring
            $length -= (int) \mb_strlen($substring);
            if ($length <= 0) {
                return $substring;
            }

            /** @var false|string $truncated - needed for PhpStan (stubs error) */
            $truncated = \mb_substr($str, 0, $length);
            if ($truncated === false) {
                return '';
            }

            // if the last word was truncated
            $space_position = \mb_strpos($str, ' ', $length - 1);
            if ($space_position !== $length) {
                // find pos of the last occurrence of a space, get up to that
                $last_position = \mb_strrpos($truncated, ' ', 0);

                if (
                    $last_position !== false
                    ||
                    (
                        $space_position !== false
                        &&
                        !$ignore_do_not_split_words_for_one_word
                    )
                ) {
                    $truncated = (string) \mb_substr($truncated, 0, (int) $last_position);
                }
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            if ($length >= (int) self::strlen($str, $encoding)) {
                return $str;
            }

            // need to further trim the string so we can append the substring
            $length -= (int) self::strlen($substring, $encoding);
            if ($length <= 0) {
                return $substring;
            }

            $truncated = self::substr($str, 0, $length, $encoding);

            if ($truncated === false) {
                return '';
            }

            // if the last word was truncated
            $space_position = self::strpos($str, ' ', $length - 1, $encoding);
            if ($space_position !== $length) {
                // find pos of the last occurrence of a space, get up to that
                $last_position = self::strrpos($truncated, ' ', 0, $encoding);

                if (
                    $last_position !== false
                    ||
                    (
                        $space_position !== false
                        &&
                        !$ignore_do_not_split_words_for_one_word
                    )
                ) {
                    $truncated = (string) self::substr($truncated, 0, (int) $last_position, $encoding);
                }
            }
        }

        return $truncated . $substring;
    }

    /**
     * Returns a lowercase and trimmed string separated by underscores.
     * Underscores are inserted before uppercase characters (with the exception
     * of the first character of the string), and in place of spaces as well as
     * dashes.
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The underscored string.</p>
     */
    public static function str_underscored(string $str): string
    {
        return self::str_delimit($str, '_');
    }

    /**
     * Returns an UpperCamelCase version of the supplied string. It trims
     * surrounding spaces, capitalizes letters following digits, spaces, dashes
     * and underscores, and removes spaces, dashes, underscores.
     *
     * @param string      $str                           <p>The input string.</p>
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string in UpperCamelCase.</p>
     */
    public static function str_upper_camelize(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        return self::ucfirst(self::str_camelize($str, $encoding), $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
    }

    /**
     * Get the number of words in a specific string.
     *
     * EXAMPLES: <code>
     * // format: 0 -> return only word count (int)
     * //
     * UTF8::str_word_count('中文空白 öäü abc#c'); // 4
     * UTF8::str_word_count('中文空白 öäü abc#c', 0, '#'); // 3
     *
     * // format: 1 -> return words (array)
     * //
     * UTF8::str_word_count('中文空白 öäü abc#c', 1); // array('中文空白', 'öäü', 'abc', 'c')
     * UTF8::str_word_count('中文空白 öäü abc#c', 1, '#'); // array('中文空白', 'öäü', 'abc#c')
     *
     * // format: 2 -> return words with offset (array)
     * //
     * UTF8::str_word_count('中文空白 öäü ab#c', 2); // array(0 => '中文空白', 5 => 'öäü', 9 => 'abc', 13 => 'c')
     * UTF8::str_word_count('中文空白 öäü ab#c', 2, '#'); // array(0 => '中文空白', 5 => 'öäü', 9 => 'abc#c')
     * </code>
     *
     * @param string $str       <p>The input string.</p>
     * @param int    $format    [optional] <p>
     *                          <strong>0</strong> => return a number of words (default)<br>
     *                          <strong>1</strong> => return an array of words<br>
     *                          <strong>2</strong> => return an array of words with word-offset as key
     *                          </p>
     * @param string $char_list [optional] <p>Additional chars that contains to words and do not start a new word.</p>
     *
     * @psalm-pure
     *
     * @return int|string[]
     *                      <p>The number of words in the string.</p>
     */
    public static function str_word_count(string $str, int $format = 0, string $char_list = '')
    {
        $str_parts = self::str_to_words($str, $char_list);

        $len = \count($str_parts);

        if ($format === 1) {
            $number_of_words = [];
            for ($i = 1; $i < $len; $i += 2) {
                $number_of_words[] = $str_parts[$i];
            }
        } elseif ($format === 2) {
            $number_of_words = [];
            $offset = (int) self::strlen($str_parts[0]);
            for ($i = 1; $i < $len; $i += 2) {
                $number_of_words[$offset] = $str_parts[$i];
                $offset += (int) self::strlen($str_parts[$i]) + (int) self::strlen($str_parts[$i + 1]);
            }
        } else {
            $number_of_words = (int) (($len - 1) / 2);
        }

        return $number_of_words;
    }

    /**
     * Case-insensitive string comparison.
     *
     * INFO: Case-insensitive version of UTF8::strcmp()
     *
     * EXAMPLE: <code>UTF8::strcasecmp("iñtërnâtiôn\nàlizætiøn", "Iñtërnâtiôn\nàlizætiøn"); // 0</code>
     *
     * @param string $str1     <p>The first string.</p>
     * @param string $str2     <p>The second string.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
     *             <strong>0</strong> if they are equal
     */
    public static function strcasecmp(
        string $str1,
        string $str2,
        string $encoding = 'UTF-8'
    ): int {
        return self::strcmp(
            self::strtocasefold(
                $str1,
                true,
                false,
                $encoding,
                null,
                false
            ),
            self::strtocasefold(
                $str2,
                true,
                false,
                $encoding,
                null,
                false
            )
        );
    }

    /**
     * Case-sensitive string comparison.
     *
     * EXAMPLE: <code>UTF8::strcmp("iñtërnâtiôn\nàlizætiøn", "iñtërnâtiôn\nàlizætiøn"); // 0</code>
     *
     * @param string $str1 <p>The first string.</p>
     * @param string $str2 <p>The second string.</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
     *             <strong>0</strong> if they are equal
     */
    public static function strcmp(string $str1, string $str2): int
    {
        if ($str1 === $str2) {
            return 0;
        }

        return \strcmp(
            /** @phpstan-ignore-next-line - we use only NFD */
            \Normalizer::normalize($str1, \Normalizer::NFD),
            /** @phpstan-ignore-next-line - we use only NFD */
            \Normalizer::normalize($str2, \Normalizer::NFD)
        );
    }

    /**
     * Find length of initial segment not matching mask.
     *
     * @param string   $str
     * @param string   $char_list
     * @param int      $offset
     * @param int|null $length
     * @param string   $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     */
    public static function strcspn(
        string $str,
        string $char_list,
        int $offset = 0,
        int $length = null,
        string $encoding = 'UTF-8'
    ): int {
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($char_list === '') {
            return (int) self::strlen($str, $encoding);
        }

        if ($offset || $length !== null) {
            if ($encoding === 'UTF-8') {
                if ($length === null) {
                    $str_tmp = \mb_substr($str, $offset);
                } else {
                    $str_tmp = \mb_substr($str, $offset, $length);
                }
            } else {
                $str_tmp = self::substr($str, $offset, $length, $encoding);
            }

            if ($str_tmp === false) {
                return 0;
            }

            $str = $str_tmp;
        }

        if ($str === '') {
            return 0;
        }

        $matches = [];
        if (\preg_match('/^(.*?)' . self::rxClass($char_list) . '/us', $str, $matches)) {
            $return = self::strlen($matches[1], $encoding);
            if ($return === false) {
                return 0;
            }

            return $return;
        }

        return (int) self::strlen($str, $encoding);
    }

    /**
     * Create a UTF-8 string from code points.
     *
     * INFO: opposite to UTF8::codepoints()
     *
     * EXAMPLE: <code>UTF8::string(array(246, 228, 252)); // 'öäü'</code>
     *
     * @param int|int[]|string|string[] $intOrHex <p>Integer or Hexadecimal codepoints.</p>
     *
     * @phpstan-param int[]|numeric-string[]|int|numeric-string $intOrHex
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A UTF-8 encoded string.</p>
     */
    public static function string($intOrHex): string
    {
        if ($intOrHex === []) {
            return '';
        }

        if (!\is_array($intOrHex)) {
            $intOrHex = [$intOrHex];
        }

        $str = '';
        foreach ($intOrHex as $strPart) {
            $str .= '&#' . (int) $strPart . ';';
        }

        // We cannot use html_entity_decode() here, as it will not return
        // characters for many values < 160.
        return mb_convert_encoding($str, 'UTF-8', 'HTML-ENTITIES');
    }

    /**
     * Checks if string starts with "BOM" (Byte Order Mark Character) character.
     *
     * EXAMPLE: <code>UTF8::string_has_bom("\xef\xbb\xbf foobar"); // true</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *              <p>
     *              <strong>true</strong> if the string has BOM at the start,<br>
     *              <strong>false</strong> otherwise
     *              </p>
     */
    public static function string_has_bom(string $str): bool
    {
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
            if (\strncmp($str, $bom_string, $bom_byte_length) === 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Strip HTML and PHP tags from a string + clean invalid UTF-8.
     *
     * EXAMPLE: <code>UTF8::strip_tags("<span>κόσμε\xa0\xa1</span>"); // 'κόσμε'</code>
     *
     * @see http://php.net/manual/en/function.strip-tags.php
     *
     * @param string      $str            <p>
     *                                    The input string.
     *                                    </p>
     * @param string|null $allowable_tags [optional] <p>
     *                                    You can use the optional second parameter to specify tags which should
     *                                    not be stripped.
     *                                    </p>
     *                                    <p>
     *                                    HTML comments and PHP tags are also stripped. This is hardcoded and
     *                                    can not be changed with allowable_tags.
     *                                    </p>
     * @param bool        $clean_utf8     [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The stripped string.</p>
     */
    public static function strip_tags(
        string $str,
        string $allowable_tags = null,
        bool $clean_utf8 = false
    ): string {
        if ($str === '') {
            return '';
        }

        if ($clean_utf8) {
            $str = self::clean($str);
        }

        if ($allowable_tags === null) {
            return \strip_tags($str);
        }

        return \strip_tags($str, $allowable_tags);
    }

    /**
     * Strip all whitespace characters. This includes tabs and newline
     * characters, as well as multibyte whitespace such as the thin space
     * and ideographic space.
     *
     * EXAMPLE: <code>UTF8::strip_whitespace('   Ο     συγγραφέας  '); // 'Οσυγγραφέας'</code>
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function strip_whitespace(string $str): string
    {
        if ($str === '') {
            return '';
        }

        return (string) \preg_replace('/[[:space:]]+/u', '', $str);
    }

    /**
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
     *
     * INFO: use UTF8::stripos_in_byte() for the byte-length
     *
     * EXAMPLE: <code>UTF8::stripos('aσσb', 'ΣΣ'); // 1</code> (σσ == ΣΣ)
     *
     * @see http://php.net/manual/en/function.mb-stripos.php
     *
     * @param string $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
     * @param string $needle     <p>The string to find in haystack.</p>
     * @param int    $offset     [optional] <p>The position in haystack to start searching.</p>
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   Return the <strong>(int)</strong> numeric position of the first occurrence of needle in the
     *                   haystack string,<br> or <strong>false</strong> if needle is not found
     */
    public static function stripos(
        string $haystack,
        string $needle,
        int $offset = 0,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000 && $needle === '') {
                return 0;
            }

            return false;
        }

        if ($needle === '' && \PHP_VERSION_ID < 80000) {
            return false;
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $haystack = self::clean($haystack);
            $needle = self::clean($needle);
        }

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_stripos($haystack, $needle, $offset);
            }

            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            return \mb_stripos($haystack, $needle, $offset, $encoding);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_stripos()" can't handle other encodings
            &&
            $offset >= 0 // grapheme_stripos() can't handle negative offset
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_stripos($haystack, $needle, $offset);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($haystack . $needle)) {
            return \stripos($haystack, $needle, $offset);
        }

        //
        // fallback via vanilla php
        //

        $haystack = self::strtocasefold($haystack, true, false, $encoding, null, false);
        $needle = self::strtocasefold($needle, true, false, $encoding, null, false);

        return self::strpos($haystack, $needle, $offset, $encoding);
    }

    /**
     * Returns all of haystack starting from and including the first occurrence of needle to the end.
     *
     * EXAMPLE: <code>
     * $str = 'iñtërnâtiônàlizætiøn';
     * $search = 'NÂT';
     *
     * UTF8::stristr($str, $search)); // 'nâtiônàlizætiøn'
     * UTF8::stristr($str, $search, true)); // 'iñtër'
     * </code>
     *
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
     * @param bool   $before_needle [optional] <p>
     *                              If <b>TRUE</b>, it returns the part of the
     *                              haystack before the first occurrence of the needle (excluding the needle).
     *                              </p>
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>A sub-string,<br>or <strong>false</strong> if needle is not found.</p>
     */
    public static function stristr(
        string $haystack,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000 && $needle === '') {
                return '';
            }

            return false;
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        if ($needle === '') {
            if (\PHP_VERSION_ID >= 80000) {
                return $haystack;
            }

            return false;
        }

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_stristr($haystack, $needle, $before_needle);
            }

            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            return \mb_stristr($haystack, $needle, $before_needle, $encoding);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::stristr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_stristr()" can't handle other encodings
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_stristr($haystack, $needle, $before_needle);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        if (ASCII::is_ascii($needle . $haystack)) {
            return \stristr($haystack, $needle, $before_needle);
        }

        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/usi', $haystack, $match);

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

        if ($before_needle) {
            return $match[1];
        }

        return self::substr($haystack, (int) self::strlen($match[1], $encoding), null, $encoding);
    }

    /**
     * Get the string length, not the byte-length!
     *
     * INFO: use UTF8::strwidth() for the char-length
     *
     * EXAMPLE: <code>UTF8::strlen("Iñtërnâtiôn\xE9àlizætiøn")); // 20</code>
     *
     * @see http://php.net/manual/en/function.mb-strlen.php
     *
     * @param string $str        <p>The string being checked for length.</p>
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>
     *                   The number <strong>(int)</strong> of characters in the string $str having character encoding
     *                   $encoding.
     *                   (One multi-byte character counted as +1).
     *                   <br>
     *                   Can return <strong>false</strong>, if e.g. mbstring is not installed and we process invalid
     *                   chars.
     *                   </p>
     */
    public static function strlen(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($str === '') {
            return 0;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($clean_utf8) {
            // "mb_strlen" and "\iconv_strlen" returns wrong length,
            // if invalid characters are found in $str
            $str = self::clean($str);
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                /** @noinspection PhpUsageOfSilenceOperatorInspection - ignore warnings, it's working anyway */
                return @\mb_strlen($str);
            }

            /** @noinspection PhpUsageOfSilenceOperatorInspection - ignore warnings, it's working anyway */
            return @\mb_strlen($str, $encoding);
        }

        //
        // fallback for binary || ascii only
        //

        if (
            $encoding === 'CP850'
            ||
            $encoding === 'ASCII'
        ) {
            return \strlen($str);
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
            &&
            self::$SUPPORT['iconv'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strlen() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via iconv
        //

        if (self::$SUPPORT['iconv'] === true) {
            $return_tmp = \iconv_strlen($str, $encoding);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback via intl
        //

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_strlen()" can't handle other encodings
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_strlen($str);
            if ($return_tmp !== null) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($str)) {
            return \strlen($str);
        }

        //
        // fallback via vanilla php
        //

        \preg_match_all('/./us', $str, $parts);

        $return_tmp = \count($parts[0]);
        if ($return_tmp === 0) {
            return false;
        }

        return $return_tmp;
    }

    /**
     * Get string length in byte.
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return int
     */
    public static function strlen_in_byte(string $str): int
    {
        if ($str === '') {
            return 0;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_strlen($str, 'CP850'); // 8-BIT
        }

        return \strlen($str);
    }

    /**
     * Case-insensitive string comparisons using a "natural order" algorithm.
     *
     * INFO: natural order version of UTF8::strcasecmp()
     *
     * EXAMPLES: <code>
     * UTF8::strnatcasecmp('2', '10Hello WORLD 中文空白!'); // -1
     * UTF8::strcasecmp('2Hello world 中文空白!', '10Hello WORLD 中文空白!'); // 1
     *
     * UTF8::strnatcasecmp('10Hello world 中文空白!', '2Hello WORLD 中文空白!'); // 1
     * UTF8::strcasecmp('10Hello world 中文空白!', '2Hello WORLD 中文空白!'); // -1
     * </code>
     *
     * @param string $str1     <p>The first string.</p>
     * @param string $str2     <p>The second string.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
     *             <strong>0</strong> if they are equal
     */
    public static function strnatcasecmp(string $str1, string $str2, string $encoding = 'UTF-8'): int
    {
        return self::strnatcmp(
            self::strtocasefold($str1, true, false, $encoding, null, false),
            self::strtocasefold($str2, true, false, $encoding, null, false)
        );
    }

    /**
     * String comparisons using a "natural order" algorithm
     *
     * INFO: natural order version of UTF8::strcmp()
     *
     * EXAMPLES: <code>
     * UTF8::strnatcmp('2Hello world 中文空白!', '10Hello WORLD 中文空白!'); // -1
     * UTF8::strcmp('2Hello world 中文空白!', '10Hello WORLD 中文空白!'); // 1
     *
     * UTF8::strnatcmp('10Hello world 中文空白!', '2Hello WORLD 中文空白!'); // 1
     * UTF8::strcmp('10Hello world 中文空白!', '2Hello WORLD 中文空白!'); // -1
     * </code>
     *
     * @see http://php.net/manual/en/function.strnatcmp.php
     *
     * @param string $str1 <p>The first string.</p>
     * @param string $str2 <p>The second string.</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
     *             <strong>&gt; 0</strong> if str1 is greater than str2;<br>
     *             <strong>0</strong> if they are equal
     */
    public static function strnatcmp(string $str1, string $str2): int
    {
        if ($str1 === $str2) {
            return 0;
        }

        return \strnatcmp(
            (string) self::strtonatfold($str1),
            (string) self::strtonatfold($str2)
        );
    }

    /**
     * Case-insensitive string comparison of the first n characters.
     *
     * EXAMPLE: <code>
     * UTF8::strcasecmp("iñtërnâtiôn\nàlizætiøn321", "iñtërnâtiôn\nàlizætiøn123", 5); // 0
     * </code>
     *
     * @see http://php.net/manual/en/function.strncasecmp.php
     *
     * @param string $str1     <p>The first string.</p>
     * @param string $str2     <p>The second string.</p>
     * @param int    $len      <p>The length of strings to be used in the comparison.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
     *             <strong>0</strong> if they are equal
     */
    public static function strncasecmp(
        string $str1,
        string $str2,
        int $len,
        string $encoding = 'UTF-8'
    ): int {
        return self::strncmp(
            self::strtocasefold($str1, true, false, $encoding, null, false),
            self::strtocasefold($str2, true, false, $encoding, null, false),
            $len
        );
    }

    /**
     * String comparison of the first n characters.
     *
     * EXAMPLE: <code>
     * UTF8::strncmp("Iñtërnâtiôn\nàlizætiøn321", "Iñtërnâtiôn\nàlizætiøn123", 5); // 0
     * </code>
     *
     * @see http://php.net/manual/en/function.strncmp.php
     *
     * @param string $str1     <p>The first string.</p>
     * @param string $str2     <p>The second string.</p>
     * @param int    $len      <p>Number of characters to use in the comparison.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
     *             <strong>0</strong> if they are equal
     */
    public static function strncmp(
        string $str1,
        string $str2,
        int $len,
        string $encoding = 'UTF-8'
    ): int {
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($encoding === 'UTF-8') {
            $str1 = (string) \mb_substr($str1, 0, $len);
            $str2 = (string) \mb_substr($str2, 0, $len);
        } else {
            $str1 = (string) self::substr($str1, 0, $len, $encoding);
            $str2 = (string) self::substr($str2, 0, $len, $encoding);
        }

        return self::strcmp($str1, $str2);
    }

    /**
     * Search a string for any of a set of characters.
     *
     * EXAMPLE: <code>UTF8::strpbrk('-中文空白-', '白'); // '白-'</code>
     *
     * @see http://php.net/manual/en/function.strpbrk.php
     *
     * @param string $haystack  <p>The string where char_list is looked for.</p>
     * @param string $char_list <p>This parameter is case-sensitive.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>The string starting from the character found, or false if it is not found.</p>
     */
    public static function strpbrk(string $haystack, string $char_list)
    {
        if ($haystack === '' || $char_list === '') {
            return false;
        }

        if (\preg_match('/' . self::rxClass($char_list) . '/us', $haystack, $m)) {
            return \substr($haystack, (int) \strpos($haystack, $m[0]));
        }

        return false;
    }

    /**
     * Find the position of the first occurrence of a substring in a string.
     *
     * INFO: use UTF8::strpos_in_byte() for the byte-length
     *
     * EXAMPLE: <code>UTF8::strpos('ABC-ÖÄÜ-中文空白-中文空白', '中'); // 8</code>
     *
     * @see http://php.net/manual/en/function.mb-strpos.php
     *
     * @param string     $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
     * @param int        $offset     [optional] <p>The search offset. If it is not specified, 0 is used.</p>
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   The <strong>(int)</strong> numeric position of the first occurrence of needle in the haystack
     *                   string.<br> If needle is not found it returns false.
     */
    public static function strpos(
        string $haystack,
        $needle,
        int $offset = 0,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000) {
                if ($needle === '') {
                    return 0;
                }
            } else {
                return false;
            }
        }

        // iconv and mbstring do not support integer $needle
        if ((int) $needle === $needle) {
            $needle = (string) self::chr($needle);
        }
        $needle = (string) $needle;

        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000 && $needle === '') {
                return 0;
            }

            return false;
        }

        if ($needle === '' && \PHP_VERSION_ID < 80000) {
            return false;
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                /** @noinspection PhpUsageOfSilenceOperatorInspection - Offset not contained in string */
                return @\mb_strpos($haystack, $needle, $offset);
            }

            /** @noinspection PhpUsageOfSilenceOperatorInspection - Offset not contained in string */
            return @\mb_strpos($haystack, $needle, $offset, $encoding);
        }

        //
        // fallback for binary || ascii only
        //
        if (
            $encoding === 'CP850'
            ||
            $encoding === 'ASCII'
        ) {
            return \strpos($haystack, $needle, $offset);
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['iconv'] === false
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strpos() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via intl
        //

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_strpos()" can't handle other encodings
            &&
            $offset >= 0 // grapheme_strpos() can't handle negative offset
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_strpos($haystack, $needle, $offset);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback via iconv
        //

        if (
            $offset >= 0 // iconv_strpos() can't handle negative offset
            &&
            self::$SUPPORT['iconv'] === true
        ) {
            // ignore invalid negative offset to keep compatibility
            // with php < 5.5.35, < 5.6.21, < 7.0.6
            $return_tmp = \iconv_strpos($haystack, $needle, $offset > 0 ? $offset : 0, $encoding);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($haystack . $needle)) {
            /** @noinspection PhpUsageOfSilenceOperatorInspection - Offset not contained in string */
            return @\strpos($haystack, $needle, $offset);
        }

        //
        // fallback via vanilla php
        //

        $haystack_tmp = self::substr($haystack, $offset, null, $encoding);
        if ($haystack_tmp === false) {
            $haystack_tmp = '';
        }
        $haystack = (string) $haystack_tmp;

        if ($offset < 0) {
            $offset = 0;
        }

        $pos = \strpos($haystack, $needle);
        if ($pos === false) {
            return false;
        }

        if ($pos) {
            return $offset + (int) self::strlen(\substr($haystack, 0, $pos), $encoding);
        }

        return $offset + 0;
    }

    /**
     * Find the position of the first occurrence of a substring in a string.
     *
     * @param string $haystack <p>
     *                         The string being checked.
     *                         </p>
     * @param string $needle   <p>
     *                         The position counted from the beginning of haystack.
     *                         </p>
     * @param int    $offset   [optional] <p>
     *                         The search offset. If it is not specified, 0 is used.
     *                         </p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The numeric position of the first occurrence of needle in the
     *                   haystack string. If needle is not found, it returns false.</p>
     */
    public static function strpos_in_byte(string $haystack, string $needle, int $offset = 0)
    {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_strpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
        }

        return \strpos($haystack, $needle, $offset);
    }

    /**
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
     *
     * @param string $haystack <p>
     *                         The string being checked.
     *                         </p>
     * @param string $needle   <p>
     *                         The position counted from the beginning of haystack.
     *                         </p>
     * @param int    $offset   [optional] <p>
     *                         The search offset. If it is not specified, 0 is used.
     *                         </p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The numeric position of the first occurrence of needle in the
     *                   haystack string. If needle is not found, it returns false.</p>
     */
    public static function stripos_in_byte(string $haystack, string $needle, int $offset = 0)
    {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_stripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
        }

        return \stripos($haystack, $needle, $offset);
    }

    /**
     * Find the last occurrence of a character in a string within another.
     *
     * EXAMPLE: <code>UTF8::strrchr('κόσμεκόσμε-äöü', 'κόσμε'); // 'κόσμε-äöü'</code>
     *
     * @see http://php.net/manual/en/function.mb-strrchr.php
     *
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
     * @param string $needle        <p>The string to find in haystack</p>
     * @param bool   $before_needle [optional] <p>
     *                              Determines which portion of haystack
     *                              this function returns.
     *                              If set to true, it returns all of haystack
     *                              from the beginning to the last occurrence of needle.
     *                              If set to false, it returns all of haystack
     *                              from the last occurrence of needle to the end,
     *                              </p>
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>The portion of haystack or false if needle is not found.</p>
     */
    public static function strrchr(
        string $haystack,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_strrchr($haystack, $needle, $before_needle);
            }

            return \mb_strrchr($haystack, $needle, $before_needle, $encoding);
        }

        //
        // fallback for binary || ascii only
        //

        if (
            !$before_needle
            &&
            (
                $encoding === 'CP850'
                ||
                $encoding === 'ASCII'
            )
        ) {
            return \strrchr($haystack, $needle);
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strrchr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via iconv
        //

        if (self::$SUPPORT['iconv'] === true) {
            $needle_tmp = self::substr($needle, 0, 1, $encoding);
            if ($needle_tmp === false) {
                return false;
            }
            $needle = $needle_tmp;

            $pos = \iconv_strrpos($haystack, $needle, $encoding);
            if ($pos === false) {
                return false;
            }

            if ($before_needle) {
                return self::substr($haystack, 0, $pos, $encoding);
            }

            return self::substr($haystack, $pos, null, $encoding);
        }

        //
        // fallback via vanilla php
        //

        $needle_tmp = self::substr($needle, 0, 1, $encoding);
        if ($needle_tmp === false) {
            return false;
        }
        $needle = $needle_tmp;

        $pos = self::strrpos($haystack, $needle, 0, $encoding);
        if ($pos === false) {
            return false;
        }

        if ($before_needle) {
            return self::substr($haystack, 0, $pos, $encoding);
        }

        return self::substr($haystack, $pos, null, $encoding);
    }

    /**
     * Reverses characters order in the string.
     *
     * EXAMPLE: <code>UTF8::strrev('κ-öäü'); // 'üäö-κ'</code>
     *
     * @param string $str      <p>The input string.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The string with characters in the reverse sequence.</p>
     */
    public static function strrev(string $str, string $encoding = 'UTF-8'): string
    {
        if ($str === '') {
            return '';
        }

        // init
        $reversed = '';

        $str = self::emoji_encode($str, true);

        if ($encoding === 'UTF-8') {
            if (self::$SUPPORT['intl'] === true) {
                // try "grapheme" first: https://stackoverflow.com/questions/17496493/strrev-dosent-support-utf-8
                $i = (int) \grapheme_strlen($str);
                while ($i--) {
                    $reversed_tmp = \grapheme_substr($str, $i, 1);
                    if ($reversed_tmp !== false) {
                        $reversed .= $reversed_tmp;
                    }
                }
            } else {
                $i = (int) \mb_strlen($str);
                while ($i--) {
                    $reversed_tmp = \mb_substr($str, $i, 1);
                    if ($reversed_tmp !== false) {
                        $reversed .= $reversed_tmp;
                    }
                }
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $i = (int) self::strlen($str, $encoding);
            while ($i--) {
                $reversed_tmp = self::substr($str, $i, 1, $encoding);
                if ($reversed_tmp !== false) {
                    $reversed .= $reversed_tmp;
                }
            }
        }

        return self::emoji_decode($reversed, true);
    }

    /**
     * Find the last occurrence of a character in a string within another, case-insensitive.
     *
     * EXAMPLE: <code>UTF8::strrichr('Aκόσμεκόσμε-äöü', 'aκόσμε'); // 'Aκόσμεκόσμε-äöü'</code>
     *
     * @see http://php.net/manual/en/function.mb-strrichr.php
     *
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
     * @param string $needle        <p>The string to find in haystack.</p>
     * @param bool   $before_needle [optional] <p>
     *                              Determines which portion of haystack
     *                              this function returns.
     *                              If set to true, it returns all of haystack
     *                              from the beginning to the last occurrence of needle.
     *                              If set to false, it returns all of haystack
     *                              from the last occurrence of needle to the end,
     *                              </p>
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>The portion of haystack or<br>false if needle is not found.</p>
     */
    public static function strrichr(
        string $haystack,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_strrichr($haystack, $needle, $before_needle);
            }

            return \mb_strrichr($haystack, $needle, $before_needle, $encoding);
        }

        //
        // fallback via vanilla php
        //

        $needle_tmp = self::substr($needle, 0, 1, $encoding);
        if ($needle_tmp === false) {
            return false;
        }
        $needle = $needle_tmp;

        $pos = self::strripos($haystack, $needle, 0, $encoding);
        if ($pos === false) {
            return false;
        }

        if ($before_needle) {
            return self::substr($haystack, 0, $pos, $encoding);
        }

        return self::substr($haystack, $pos, null, $encoding);
    }

    /**
     * Find the position of the last occurrence of a substring in a string, case-insensitive.
     *
     * EXAMPLE: <code>UTF8::strripos('ABC-ÖÄÜ-中文空白-中文空白', '中'); // 13</code>
     *
     * @param string     $haystack   <p>The string to look in.</p>
     * @param int|string $needle     <p>The string to look for.</p>
     * @param int        $offset     [optional] <p>Number of characters to ignore in the beginning or end.</p>
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
     *                   string.<br>If needle is not found, it returns false.</p>
     */
    public static function strripos(
        string $haystack,
        $needle,
        int $offset = 0,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000) {
                if ($needle === '') {
                    return 0;
                }
            } else {
                return false;
            }
        }

        // iconv and mbstring do not support integer $needle
        if ((int) $needle === $needle && $needle >= 0) {
            $needle = (string) self::chr($needle);
        }
        $needle = (string) $needle;

        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000 && $needle === '') {
                return 0;
            }

            return false;
        }

        if ($needle === '' && \PHP_VERSION_ID < 80000) {
            return false;
        }

        if ($clean_utf8) {
            // mb_strripos() && iconv_strripos() is not tolerant to invalid characters
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        //
        // fallback via mbstrig
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_strripos($haystack, $needle, $offset);
            }

            return \mb_strripos($haystack, $needle, $offset, $encoding);
        }

        //
        // fallback for binary || ascii only
        //

        if (
            $encoding === 'CP850'
            ||
            $encoding === 'ASCII'
        ) {
            return \strripos($haystack, $needle, $offset);
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strripos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via intl
        //

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_strripos()" can't handle other encodings
            &&
            $offset >= 0 // grapheme_strripos() can't handle negative offset
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_strripos($haystack, $needle, $offset);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($haystack . $needle)) {
            return \strripos($haystack, $needle, $offset);
        }

        //
        // fallback via vanilla php
        //

        $haystack = self::strtocasefold($haystack, true, false, $encoding);
        $needle = self::strtocasefold($needle, true, false, $encoding);

        return self::strrpos($haystack, $needle, $offset, $encoding, $clean_utf8);
    }

    /**
     * Finds position of last occurrence of a string within another, case-insensitive.
     *
     * @param string $haystack <p>
     *                         The string from which to get the position of the last occurrence
     *                         of needle.
     *                         </p>
     * @param string $needle   <p>
     *                         The string to find in haystack.
     *                         </p>
     * @param int    $offset   [optional] <p>
     *                         The position in haystack
     *                         to start searching.
     *                         </p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>eturn the numeric position of the last occurrence of needle in the
     *                   haystack string, or false if needle is not found.</p>
     */
    public static function strripos_in_byte(string $haystack, string $needle, int $offset = 0)
    {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_strripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
        }

        return \strripos($haystack, $needle, $offset);
    }

    /**
     * Find the position of the last occurrence of a substring in a string.
     *
     * EXAMPLE: <code>UTF8::strrpos('ABC-ÖÄÜ-中文空白-中文空白', '中'); // 13</code>
     *
     * @see http://php.net/manual/en/function.mb-strrpos.php
     *
     * @param string     $haystack   <p>The string being checked, for the last occurrence of needle</p>
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
     * @param int        $offset     [optional] <p>May be specified to begin searching an arbitrary number of characters
     *                               into the string. Negative values will stop searching at an arbitrary point prior to
     *                               the end of the string.
     *                               </p>
     * @param string     $encoding   [optional] <p>Set the charset.</p>
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
     *                   string.<br>If needle is not found, it returns false.</p>
     */
    public static function strrpos(
        string $haystack,
        $needle,
        int $offset = 0,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000) {
                if ($needle === '') {
                    return 0;
                }
            } else {
                return false;
            }
        }

        // iconv and mbstring do not support integer $needle
        if ((int) $needle === $needle && $needle >= 0) {
            $needle = (string) self::chr($needle);
        }
        $needle = (string) $needle;

        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000 && $needle === '') {
                return 0;
            }

            return false;
        }

        if ($needle === '' && \PHP_VERSION_ID < 80000) {
            return false;
        }

        if ($clean_utf8) {
            // mb_strrpos && iconv_strrpos is not tolerant to invalid characters
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_strrpos($haystack, $needle, $offset);
            }

            return \mb_strrpos($haystack, $needle, $offset, $encoding);
        }

        //
        // fallback for binary || ascii only
        //

        if (
            $encoding === 'CP850'
            ||
            $encoding === 'ASCII'
        ) {
            return \strrpos($haystack, $needle, $offset);
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strrpos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via intl
        //

        if (
            $offset >= 0 // grapheme_strrpos() can't handle negative offset
            &&
            $encoding === 'UTF-8' // INFO: "grapheme_strrpos()" can't handle other encodings
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_strrpos($haystack, $needle, $offset);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($haystack . $needle)) {
            return \strrpos($haystack, $needle, $offset);
        }

        //
        // fallback via vanilla php
        //

        $haystack_tmp = null;
        if ($offset > 0) {
            $haystack_tmp = self::substr($haystack, $offset);
        } elseif ($offset < 0) {
            $haystack_tmp = self::substr($haystack, 0, $offset);
            $offset = 0;
        }

        if ($haystack_tmp !== null) {
            if ($haystack_tmp === false) {
                $haystack_tmp = '';
            }
            $haystack = (string) $haystack_tmp;
        }

        $pos = \strrpos($haystack, $needle);
        if ($pos === false) {
            return false;
        }

        /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
        $str_tmp = \substr($haystack, 0, $pos);
        if ($str_tmp === false) {
            return false;
        }

        return $offset + (int) self::strlen($str_tmp);
    }

    /**
     * Find the position of the last occurrence of a substring in a string.
     *
     * @param string $haystack <p>
     *                         The string being checked, for the last occurrence
     *                         of needle.
     *                         </p>
     * @param string $needle   <p>
     *                         The string to find in haystack.
     *                         </p>
     * @param int    $offset   [optional] <p>May be specified to begin searching an arbitrary number of characters into
     *                         the string. Negative values will stop searching at an arbitrary point
     *                         prior to the end of the string.
     *                         </p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The numeric position of the last occurrence of needle in the
     *                   haystack string. If needle is not found, it returns false.</p>
     */
    public static function strrpos_in_byte(string $haystack, string $needle, int $offset = 0)
    {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_strrpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
        }

        return \strrpos($haystack, $needle, $offset);
    }

    /**
     * Finds the length of the initial segment of a string consisting entirely of characters contained within a given
     * mask.
     *
     * EXAMPLE: <code>UTF8::strspn('iñtërnâtiônàlizætiøn', 'itñ'); // '3'</code>
     *
     * @param string   $str      <p>The input string.</p>
     * @param string   $mask     <p>The mask of chars</p>
     * @param int      $offset   [optional]
     * @param int|null $length   [optional]
     * @param string   $encoding [optional] <p>Set the charset.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     */
    public static function strspn(
        string $str,
        string $mask,
        int $offset = 0,
        int $length = null,
        string $encoding = 'UTF-8'
    ) {
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($offset || $length !== null) {
            if ($encoding === 'UTF-8') {
                if ($length === null) {
                    $str = (string) \mb_substr($str, $offset);
                } else {
                    $str = (string) \mb_substr($str, $offset, $length);
                }
            } else {
                $str = (string) self::substr($str, $offset, $length, $encoding);
            }
        }

        if ($str === '' || $mask === '') {
            return 0;
        }

        $matches = [];

        return \preg_match('/^' . self::rxClass($mask) . '+/u', $str, $matches) ? (int) self::strlen($matches[0], $encoding) : 0;
    }

    /**
     * Returns part of haystack string from the first occurrence of needle to the end of haystack.
     *
     * EXAMPLE: <code>
     * $str = 'iñtërnâtiônàlizætiøn';
     * $search = 'nât';
     *
     * UTF8::strstr($str, $search)); // 'nâtiônàlizætiøn'
     * UTF8::strstr($str, $search, true)); // 'iñtër'
     * </code>
     *
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
     * @param bool   $before_needle [optional] <p>
     *                              If <b>TRUE</b>, strstr() returns the part of the
     *                              haystack before the first occurrence of the needle (excluding the needle).
     *                              </p>
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>A sub-string,<br>or <strong>false</strong> if needle is not found.</p>
     */
    public static function strstr(
        string $haystack,
        string $needle,
        bool $before_needle = false,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000 && $needle === '') {
                return '';
            }

            return false;
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        if ($needle === '') {
            if (\PHP_VERSION_ID >= 80000) {
                return $haystack;
            }

            return false;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_strstr($haystack, $needle, $before_needle);
            }

            return \mb_strstr($haystack, $needle, $before_needle, $encoding);
        }

        //
        // fallback for binary || ascii only
        //

        if (
            $encoding === 'CP850'
            ||
            $encoding === 'ASCII'
        ) {
            return \strstr($haystack, $needle, $before_needle);
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strstr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via intl
        //

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_strstr()" can't handle other encodings
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_strstr($haystack, $needle, $before_needle);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($haystack . $needle)) {
            return \strstr($haystack, $needle, $before_needle);
        }

        //
        // fallback via vanilla php
        //

        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/us', $haystack, $match);

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

        if ($before_needle) {
            return $match[1];
        }

        return self::substr($haystack, (int) self::strlen($match[1]));
    }

    /**
     * Finds first occurrence of a string within another.
     *
     * @param string $haystack      <p>
     *                              The string from which to get the first occurrence
     *                              of needle.
     *                              </p>
     * @param string $needle        <p>
     *                              The string to find in haystack.
     *                              </p>
     * @param bool   $before_needle [optional] <p>
     *                              Determines which portion of haystack
     *                              this function returns.
     *                              If set to true, it returns all of haystack
     *                              from the beginning to the first occurrence of needle.
     *                              If set to false, it returns all of haystack
     *                              from the first occurrence of needle to the end,
     *                              </p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      <p>The portion of haystack,
     *                      or false if needle is not found.</p>
     */
    public static function strstr_in_byte(
        string $haystack,
        string $needle,
        bool $before_needle = false
    ) {
        if ($haystack === '' || $needle === '') {
            return false;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_strstr($haystack, $needle, $before_needle, 'CP850'); // 8-BIT
        }

        return \strstr($haystack, $needle, $before_needle);
    }

    /**
     * Unicode transformation for case-less matching.
     *
     * EXAMPLE: <code>UTF8::strtocasefold('ǰ◌̱'); // 'ǰ◌̱'</code>
     *
     * @see http://unicode.org/reports/tr21/tr21-5.html
     *
     * @param string      $str        <p>The input string.</p>
     * @param bool        $full       [optional] <p>
     *                                <b>true</b>, replace full case folding chars (default)<br>
     *                                <b>false</b>, use only limited static array [UTF8::$COMMON_CASE_FOLD]
     *                                </p>
     * @param bool        $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string      $encoding   [optional] <p>Set the charset.</p>
     * @param string|null $lang       [optional] <p>Set the language for special cases: az, el, lt, tr</p>
     * @param bool        $lower      [optional] <p>Use lowercase string, otherwise use uppercase string. PS: uppercase
     *                                is for some languages better ...</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function strtocasefold(
        string $str,
        bool $full = true,
        bool $clean_utf8 = false,
        string $encoding = 'UTF-8',
        string $lang = null,
        bool $lower = true
    ): string {
        if ($str === '') {
            return '';
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        $str = self::fixStrCaseHelper($str, $lower, $full);

        if ($lang === null && $encoding === 'UTF-8') {
            if ($lower) {
                return \mb_strtolower($str);
            }

            return \mb_strtoupper($str);
        }

        if ($lower) {
            return self::strtolower($str, $encoding, false, $lang);
        }

        return self::strtoupper($str, $encoding, false, $lang);
    }

    /**
     * Make a string lowercase.
     *
     * EXAMPLE: <code>UTF8::strtolower('DÉJÀ Σσς Iıİi'); // 'déjà σσς iıii'</code>
     *
     * @see http://php.net/manual/en/function.mb-strtolower.php
     *
     * @param string      $str                           <p>The string being lowercased.</p>
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>String with all alphabetic characters converted to lowercase.</p>
     */
    public static function strtolower(
        $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        // init
        $str = (string) $str;

        if ($str === '') {
            return '';
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        // hack for old php version or for the polyfill ...
        if ($try_to_keep_the_string_length) {
            $str = self::fixStrCaseHelper($str, true);
        }

        if ($lang === null && $encoding === 'UTF-8') {
            return \mb_strtolower($str);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if ($lang !== null) {
            if (self::$SUPPORT['intl'] === true) {
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
                }

                $language_code = $lang . '-Lower';
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
                    /**
                     * @psalm-suppress ImpureFunctionCall - is is only a warning
                     */
                    \trigger_error('UTF8::strtolower() cannot handle special language: ' . $lang . ' | supported: ' . \print_r(self::$INTL_TRANSLITERATOR_LIST, true), \E_USER_WARNING);

                    $language_code = 'Any-Lower';
                }

                return (string) \transliterator_transliterate($language_code, $str);
            }

            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang" parameter: ' . $lang, \E_USER_WARNING);
        }

        // always fallback via symfony polyfill
        return \mb_strtolower($str, $encoding);
    }

    /**
     * Make a string uppercase.
     *
     * EXAMPLE: <code>UTF8::strtoupper('Déjà Σσς Iıİi'); // 'DÉJÀ ΣΣΣ IIİI'</code>
     *
     * @see http://php.net/manual/en/function.mb-strtoupper.php
     *
     * @param string      $str                           <p>The string being uppercased.</p>
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>String with all alphabetic characters converted to uppercase.</p>
     */
    public static function strtoupper(
        $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        // init
        $str = (string) $str;

        if ($str === '') {
            return '';
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        // hack for old php version or for the polyfill ...
        if ($try_to_keep_the_string_length) {
            $str = self::fixStrCaseHelper($str);
        }

        if ($lang === null && $encoding === 'UTF-8') {
            return \mb_strtoupper($str);
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if ($lang !== null) {
            if (self::$SUPPORT['intl'] === true) {
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
                }

                $language_code = $lang . '-Upper';
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
                    /**
                     * @psalm-suppress ImpureFunctionCall - is is only a warning
                     */
                    \trigger_error('UTF8::strtoupper() without intl for special language: ' . $lang, \E_USER_WARNING);

                    $language_code = 'Any-Upper';
                }

                return (string) \transliterator_transliterate($language_code, $str);
            }

            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang"-parameter: ' . $lang, \E_USER_WARNING);
        }

        // always fallback via symfony polyfill
        return \mb_strtoupper($str, $encoding);
    }

    /**
     * Translate characters or replace sub-strings.
     *
     * EXAMPLE:
     * <code>
     * $array = [
     *     'Hello'   => '○●◎',
     *     '中文空白' => 'earth',
     * ];
     * UTF8::strtr('Hello 中文空白', $array); // '○●◎ earth'
     * </code>
     *
     * @see http://php.net/manual/en/function.strtr.php
     *
     * @param string          $str  <p>The string being translated.</p>
     * @param string|string[] $from <p>The string replacing from.</p>
     * @param string|string[] $to   [optional] <p>The string being translated to to.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>This function returns a copy of str, translating all occurrences of each character in "from"
     *                to the corresponding character in "to".</p>
     */
    public static function strtr(string $str, $from, $to = ''): string
    {
        if ($str === '') {
            return '';
        }

        if ($from === $to) {
            return $str;
        }

        if ($to !== '') {
            if (!\is_array($from)) {
                $from = self::str_split($from);
            }

            if (!\is_array($to)) {
                $to = self::str_split($to);
            }

            $count_from = \count($from);
            $count_to = \count($to);

            if ($count_from !== $count_to) {
                if ($count_from > $count_to) {
                    $from = \array_slice($from, 0, $count_to);
                } elseif ($count_from < $count_to) {
                    $to = \array_slice($to, 0, $count_from);
                }
            }

            $from = \array_combine($from, $to);
            if ($from === false) {
                throw new \InvalidArgumentException('The number of elements for each array isn\'t equal or the arrays are empty: (from: ' . \print_r($from, true) . ' | to: ' . \print_r($to, true) . ')');
            }
        }

        if (\is_string($from)) {
            return \str_replace($from, $to, $str);
        }

        return \strtr($str, $from);
    }

    /**
     * Return the width of a string.
     *
     * INFO: use UTF8::strlen() for the byte-length
     *
     * EXAMPLE: <code>UTF8::strwidth("Iñtërnâtiôn\xE9àlizætiøn")); // 21</code>
     *
     * @param string $str        <p>The input string.</p>
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return int
     */
    public static function strwidth(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ): int {
        if ($str === '') {
            return 0;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($clean_utf8) {
            // iconv and mbstring are not tolerant to invalid encoding
            // further, their behaviour is inconsistent with that of PHP's substr
            $str = self::clean($str);
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_strwidth($str);
            }

            return \mb_strwidth($str, $encoding);
        }

        //
        // fallback via vanilla php
        //

        if ($encoding !== 'UTF-8') {
            $str = self::encode('UTF-8', $str, false, $encoding);
        }

        $wide = 0;
        $str = (string) \preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $str, -1, $wide);

        return ($wide << 1) + (int) self::strlen($str);
    }

    /**
     * Get part of a string.
     *
     * EXAMPLE: <code>UTF8::substr('中文空白', 1, 2); // '文空'</code>
     *
     * @see http://php.net/manual/en/function.mb-substr.php
     *
     * @param string   $str        <p>The string being checked.</p>
     * @param int      $offset     <p>The first position used in str.</p>
     * @param int|null $length     [optional] <p>The maximum length of the returned string.</p>
     * @param string   $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
     *                      characters long, <b>FALSE</b> will be returned.
     */
    public static function substr(
        string $str,
        int $offset = 0,
        int $length = null,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        // empty string
        if ($str === '' || $length === 0) {
            return '';
        }

        if ($clean_utf8) {
            // iconv and mbstring are not tolerant to invalid encoding
            // further, their behaviour is inconsistent with that of PHP's substr
            $str = self::clean($str);
        }

        // whole string
        if (!$offset && $length === null) {
            return $str;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        //
        // fallback via mbstring
        //

        if (self::$SUPPORT['mbstring'] === true && $encoding === 'UTF-8') {
            if ($length === null) {
                return \mb_substr($str, $offset);
            }

            return \mb_substr($str, $offset, $length);
        }

        //
        // fallback for binary || ascii only
        //

        if (
            $encoding === 'CP850'
            ||
            $encoding === 'ASCII'
        ) {
            if ($length === null) {
                return \substr($str, $offset);
            }

            return \substr($str, $offset, $length);
        }

        // otherwise we need the string-length
        $str_length = 0;
        if ($offset || $length === null) {
            $str_length = self::strlen($str, $encoding);
        }

        // e.g.: invalid chars + mbstring not installed
        if ($str_length === false) {
            return false;
        }

        // empty string
        if ($offset === $str_length && !$length) {
            return '';
        }

        // impossible
        if ($offset && $offset > $str_length) {
            return '';
        }

        $length = $length ?? $str_length;

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::substr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        //
        // fallback via intl
        //

        if (
            $encoding === 'UTF-8' // INFO: "grapheme_substr()" can't handle other encodings
            &&
            $offset >= 0 // grapheme_substr() can't handle negative offset
            &&
            self::$SUPPORT['intl'] === true
        ) {
            $return_tmp = \grapheme_substr($str, $offset, $length);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback via iconv
        //

        if (
            $length >= 0 // "iconv_substr()" can't handle negative length
            &&
            self::$SUPPORT['iconv'] === true
        ) {
            $return_tmp = \iconv_substr($str, $offset, $length);
            if ($return_tmp !== false) {
                return $return_tmp;
            }
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($str)) {
            return \substr($str, $offset, $length);
        }

        //
        // fallback via vanilla php
        //

        // split to array, and remove invalid characters
        // &&
        // extract relevant part, and join to make sting again
        return \implode('', \array_slice(self::str_split($str), $offset, $length));
    }

    /**
     * Binary-safe comparison of two strings from an offset, up to a length of characters.
     *
     * EXAMPLE: <code>
     * UTF8::substr_compare("○●◎\r", '●◎', 0, 2); // -1
     * UTF8::substr_compare("○●◎\r", '◎●', 1, 2); // 1
     * UTF8::substr_compare("○●◎\r", '●◎', 1, 2); // 0
     * </code>
     *
     * @param string   $str1               <p>The main string being compared.</p>
     * @param string   $str2               <p>The secondary string being compared.</p>
     * @param int      $offset             [optional] <p>The start position for the comparison. If negative, it starts
     *                                     counting from the end of the string.</p>
     * @param int|null $length             [optional] <p>The length of the comparison. The default value is the largest
     *                                     of the length of the str compared to the length of main_str less the
     *                                     offset.</p>
     * @param bool     $case_insensitivity [optional] <p>If case_insensitivity is TRUE, comparison is case
     *                                     insensitive.</p>
     * @param string   $encoding           [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
     *             <strong>0</strong> if they are equal
     */
    public static function substr_compare(
        string $str1,
        string $str2,
        int $offset = 0,
        int $length = null,
        bool $case_insensitivity = false,
        string $encoding = 'UTF-8'
    ): int {
        if (
            $offset !== 0
            ||
            $length !== null
        ) {
            if ($encoding === 'UTF-8') {
                if ($length === null) {
                    $str1 = (string) \mb_substr($str1, $offset);
                } else {
                    $str1 = (string) \mb_substr($str1, $offset, $length);
                }
                $str2 = (string) \mb_substr($str2, 0, (int) self::strlen($str1));
            } else {
                $encoding = self::normalize_encoding($encoding, 'UTF-8');

                $str1 = (string) self::substr($str1, $offset, $length, $encoding);
                $str2 = (string) self::substr($str2, 0, (int) self::strlen($str1), $encoding);
            }
        }

        if ($case_insensitivity) {
            return self::strcasecmp($str1, $str2, $encoding);
        }

        return self::strcmp($str1, $str2);
    }

    /**
     * Count the number of substring occurrences.
     *
     * EXAMPLE: <code>UTF8::substr_count('中文空白', '文空', 1, 2); // 1</code>
     *
     * @see http://php.net/manual/en/function.substr-count.php
     *
     * @param string   $haystack   <p>The string to search in.</p>
     * @param string   $needle     <p>The substring to search for.</p>
     * @param int      $offset     [optional] <p>The offset where to start counting.</p>
     * @param int|null $length     [optional] <p>
     *                             The maximum length after the specified offset to search for the
     *                             substring. It outputs a warning if the offset plus the length is
     *                             greater than the haystack length.
     *                             </p>
     * @param string   $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>This functions returns an integer or false if there isn't a string.</p>
     */
    public static function substr_count(
        string $haystack,
        string $needle,
        int $offset = 0,
        int $length = null,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ) {
        if ($needle === '') {
            return false;
        }

        if ($haystack === '') {
            if (\PHP_VERSION_ID >= 80000) {
                return 0;
            }

            return 0;
        }

        if ($length === 0) {
            return 0;
        }

        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $needle = self::clean($needle);
            $haystack = self::clean($haystack);
        }

        if ($offset || $length > 0) {
            if ($length === null) {
                $length_tmp = self::strlen($haystack, $encoding);
                if ($length_tmp === false) {
                    return false;
                }
                $length = $length_tmp;
            }

            if ($encoding === 'UTF-8') {
                $haystack = (string) \mb_substr($haystack, $offset, $length);
            } else {
                $haystack = (string) \mb_substr($haystack, $offset, $length, $encoding);
            }
        }

        if (
            $encoding !== 'UTF-8'
            &&
            self::$SUPPORT['mbstring'] === false
        ) {
            /**
             * @psalm-suppress ImpureFunctionCall - is is only a warning
             */
            \trigger_error('UTF8::substr_count() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
        }

        if (self::$SUPPORT['mbstring'] === true) {
            if ($encoding === 'UTF-8') {
                return \mb_substr_count($haystack, $needle);
            }

            return \mb_substr_count($haystack, $needle, $encoding);
        }

        \preg_match_all('/' . \preg_quote($needle, '/') . '/us', $haystack, $matches, \PREG_SET_ORDER);

        return \count($matches);
    }

    /**
     * Count the number of substring occurrences.
     *
     * @param string   $haystack <p>
     *                           The string being checked.
     *                           </p>
     * @param string   $needle   <p>
     *                           The string being found.
     *                           </p>
     * @param int      $offset   [optional] <p>
     *                           The offset where to start counting
     *                           </p>
     * @param int|null $length   [optional] <p>
     *                           The maximum length after the specified offset to search for the
     *                           substring. It outputs a warning if the offset plus the length is
     *                           greater than the haystack length.
     *                           </p>
     *
     * @psalm-pure
     *
     * @return false|int
     *                   <p>The number of times the
     *                   needle substring occurs in the
     *                   haystack string.</p>
     */
    public static function substr_count_in_byte(
        string $haystack,
        string $needle,
        int $offset = 0,
        int $length = null
    ) {
        if ($haystack === '' || $needle === '') {
            return 0;
        }

        if (
            ($offset || $length !== null)
            &&
            self::$SUPPORT['mbstring_func_overload'] === true
        ) {
            if ($length === null) {
                $length_tmp = self::strlen($haystack);
                if ($length_tmp === false) {
                    return false;
                }
                $length = $length_tmp;
            }

            if (
                (
                    $length !== 0
                    &&
                    $offset !== 0
                )
                &&
                ($length + $offset) <= 0
                &&
                \PHP_VERSION_ID < 71000 // output from "substr_count()" have changed in PHP 7.1
            ) {
                return false;
            }

            /** @var false|string $haystack_tmp - needed for PhpStan (stubs error) */
            $haystack_tmp = \substr($haystack, $offset, $length);
            if ($haystack_tmp === false) {
                $haystack_tmp = '';
            }
            $haystack = (string) $haystack_tmp;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_substr_count($haystack, $needle, 'CP850'); // 8-BIT
        }

        if ($length === null) {
            return \substr_count($haystack, $needle, $offset);
        }

        return \substr_count($haystack, $needle, $offset, $length);
    }

    /**
     * Returns the number of occurrences of $substring in the given string.
     * By default, the comparison is case-sensitive, but can be made insensitive
     * by setting $case_sensitive to false.
     *
     * @param string $str            <p>The input string.</p>
     * @param string $substring      <p>The substring to search for.</p>
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return int
     */
    public static function substr_count_simple(
        string $str,
        string $substring,
        bool $case_sensitive = true,
        string $encoding = 'UTF-8'
    ): int {
        if ($str === '' || $substring === '') {
            return 0;
        }

        if ($encoding === 'UTF-8') {
            if ($case_sensitive) {
                return (int) \mb_substr_count($str, $substring);
            }

            return (int) \mb_substr_count(
                \mb_strtoupper($str),
                \mb_strtoupper($substring)
            );
        }

        $encoding = self::normalize_encoding($encoding, 'UTF-8');

        if ($case_sensitive) {
            return (int) \mb_substr_count($str, $substring, $encoding);
        }

        return (int) \mb_substr_count(
            self::strtocasefold($str, true, false, $encoding, null, false),
            self::strtocasefold($substring, true, false, $encoding, null, false),
            $encoding
        );
    }

    /**
     * Removes a prefix ($needle) from the beginning of the string ($haystack), case-insensitive.
     *
     * EXMAPLE: <code>
     * UTF8::substr_ileft('ΚόσμεMiddleEnd', 'Κόσμε'); // 'MiddleEnd'
     * UTF8::substr_ileft('ΚόσμεMiddleEnd', 'κόσμε'); // 'MiddleEnd'
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Return the sub-string.</p>
     */
    public static function substr_ileft(string $haystack, string $needle): string
    {
        if ($haystack === '') {
            return '';
        }

        if ($needle === '') {
            return $haystack;
        }

        if (self::str_istarts_with($haystack, $needle)) {
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
        }

        return $haystack;
    }

    /**
     * Get part of a string process in bytes.
     *
     * @param string   $str    <p>The string being checked.</p>
     * @param int      $offset <p>The first position used in str.</p>
     * @param int|null $length [optional] <p>The maximum length of the returned string.</p>
     *
     * @psalm-pure
     *
     * @return false|string
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
     *                      characters long, <b>FALSE</b> will be returned.
     */
    public static function substr_in_byte(string $str, int $offset = 0, int $length = null)
    {
        // empty string
        if ($str === '' || $length === 0) {
            return '';
        }

        // whole string
        if (!$offset && $length === null) {
            return $str;
        }

        if (self::$SUPPORT['mbstring_func_overload'] === true) {
            // "mb_" is available if overload is used, so use it ...
            return \mb_substr($str, $offset, $length, 'CP850'); // 8-BIT
        }

        return \substr($str, $offset, $length ?? 2147483647);
    }

    /**
     * Removes a suffix ($needle) from the end of the string ($haystack), case-insensitive.
     *
     * EXAMPLE: <code>
     * UTF8::substr_iright('BeginMiddleΚόσμε', 'Κόσμε'); // 'BeginMiddle'
     * UTF8::substr_iright('BeginMiddleΚόσμε', 'κόσμε'); // 'BeginMiddle'
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Return the sub-string.<p>
     */
    public static function substr_iright(string $haystack, string $needle): string
    {
        if ($haystack === '') {
            return '';
        }

        if ($needle === '') {
            return $haystack;
        }

        if (self::str_iends_with($haystack, $needle)) {
            $haystack = (string) \mb_substr($haystack, 0, (int) self::strlen($haystack) - (int) self::strlen($needle));
        }

        return $haystack;
    }

    /**
     * Removes a prefix ($needle) from the beginning of the string ($haystack).
     *
     * EXAMPLE: <code>
     * UTF8::substr_left('ΚόσμεMiddleEnd', 'Κόσμε'); // 'MiddleEnd'
     * UTF8::substr_left('ΚόσμεMiddleEnd', 'κόσμε'); // 'ΚόσμεMiddleEnd'
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Return the sub-string.</p>
     */
    public static function substr_left(string $haystack, string $needle): string
    {
        if ($haystack === '') {
            return '';
        }

        if ($needle === '') {
            return $haystack;
        }

        if (self::str_starts_with($haystack, $needle)) {
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
        }

        return $haystack;
    }

    /**
     * Replace text within a portion of a string.
     *
     * EXAMPLE: <code>UTF8::substr_replace(array('Iñtërnâtiônàlizætiøn', 'foo'), 'æ', 1); // array('Iæñtërnâtiônàlizætiøn', 'fæoo')</code>
     *
     * source: https://gist.github.com/stemar/8287074
     *
     * @param string|string[] $str         <p>The input string or an array of stings.</p>
     * @param string|string[] $replacement <p>The replacement string or an array of stings.</p>
     * @param int|int[]       $offset      <p>
     *                                     If start is positive, the replacing will begin at the start'th offset
     *                                     into string.
     *                                     <br><br>
     *                                     If start is negative, the replacing will begin at the start'th character
     *                                     from the end of string.
     *                                     </p>
     * @param int|int[]|null  $length      [optional] <p>If given and is positive, it represents the length of the
     *                                     portion of string which is to be replaced. If it is negative, it
     *                                     represents the number of characters from the end of string at which to
     *                                     stop replacing. If it is not given, then it will default to strlen(
     *                                     string ); i.e. end the replacing at the end of string. Of course, if
     *                                     length is zero then this function will have the effect of inserting
     *                                     replacement into string at the given start offset.</p>
     * @param string          $encoding    [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string|string[]
     *                         <p>The result string is returned. If string is an array then array is returned.</p>
     *
     * @template TSubstrReplace
     * @phpstan-param TSubstrReplace $str
     * @phpstan-return TSubstrReplace
     */
    public static function substr_replace(
        $str,
        $replacement,
        $offset,
        $length = null,
        string $encoding = 'UTF-8'
    ) {
        if (\is_array($str)) {
            $num = \count($str);

            // the replacement
            if (\is_array($replacement)) {
                $replacement = \array_slice($replacement, 0, $num);
            } else {
                $replacement = \array_pad([$replacement], $num, $replacement);
            }

            // the offset
            if (\is_array($offset)) {
                $offset = \array_slice($offset, 0, $num);
                foreach ($offset as &$value_tmp) {
                    $value_tmp = (int) $value_tmp === $value_tmp ? $value_tmp : 0;
                }
                unset($value_tmp);
            } else {
                $offset = \array_pad([$offset], $num, $offset);
            }

            // the length
            if ($length === null) {
                $length = \array_fill(0, $num, 0);
            } elseif (\is_array($length)) {
                $length = \array_slice($length, 0, $num);
                foreach ($length as &$value_tmp_V2) {
                    $value_tmp_V2 = (int) $value_tmp_V2 === $value_tmp_V2 ? $value_tmp_V2 : $num;
                }
                unset($value_tmp_V2);
            } else {
                $length = \array_pad([$length], $num, $length);
            }

            // recursive call
            /** @phpstan-ignore-next-line - phpstan currently can't handle recursive calls */
            return \array_map([self::class, 'substr_replace'], $str, $replacement, $offset, $length);
        }

        if (\is_array($replacement)) {
            if ($replacement !== []) {
                $replacement = $replacement[0];
            } else {
                $replacement = '';
            }
        }

        // init
        $str = (string) $str;
        $replacement = (string) $replacement;

        if (\is_array($length)) {
            throw new \InvalidArgumentException('Parameter "$length" can only be an array, if "$str" is also an array.');
        }

        if (\is_array($offset)) {
            throw new \InvalidArgumentException('Parameter "$offset" can only be an array, if "$str" is also an array.');
        }

        if ($str === '') {
            return $replacement;
        }

        if (self::$SUPPORT['mbstring'] === true) {
            $string_length = (int) self::strlen($str, $encoding);

            if ($offset < 0) {
                $offset = (int) \max(0, $string_length + $offset);
            } elseif ($offset > $string_length) {
                $offset = $string_length;
            }

            if ($length !== null && $length < 0) {
                $length = (int) \max(0, $string_length - $offset + $length);
            } elseif ($length === null || $length > $string_length) {
                $length = $string_length;
            }

            if (($offset + $length) > $string_length) {
                $length = $string_length - $offset;
            }

            return ((string) \mb_substr($str, 0, $offset, $encoding)) .
                   $replacement .
                   ((string) \mb_substr($str, $offset + $length, $string_length - $offset - $length, $encoding));
        }

        //
        // fallback for ascii only
        //

        if (ASCII::is_ascii($str)) {
            return ($length === null) ?
                \substr_replace($str, $replacement, $offset) :
                \substr_replace($str, $replacement, $offset, $length);
        }

        //
        // fallback via vanilla php
        //

        \preg_match_all('/./us', $str, $str_matches);
        \preg_match_all('/./us', $replacement, $replacement_matches);

        if ($length === null) {
            $length_tmp = self::strlen($str, $encoding);
            if ($length_tmp === false) {
                // e.g.: non mbstring support + invalid chars
                return '';
            }
            $length = $length_tmp;
        }

        \array_splice($str_matches[0], $offset, $length, $replacement_matches[0]);

        return \implode('', $str_matches[0]);
    }

    /**
     * Removes a suffix ($needle) from the end of the string ($haystack).
     *
     * EXAMPLE: <code>
     * UTF8::substr_right('BeginMiddleΚόσμε', 'Κόσμε'); // 'BeginMiddle'
     * UTF8::substr_right('BeginMiddleΚόσμε', 'κόσμε'); // 'BeginMiddleΚόσμε'
     * </code>
     *
     * @param string $haystack <p>The string to search in.</p>
     * @param string $needle   <p>The substring to search for.</p>
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Return the sub-string.</p>
     */
    public static function substr_right(
        string $haystack,
        string $needle,
        string $encoding = 'UTF-8'
    ): string {
        if ($haystack === '') {
            return '';
        }

        if ($needle === '') {
            return $haystack;
        }

        if (
            $encoding === 'UTF-8'
            &&
            \substr($haystack, -\strlen($needle)) === $needle
        ) {
            return (string) \mb_substr($haystack, 0, (int) \mb_strlen($haystack) - (int) \mb_strlen($needle));
        }

        if (\substr($haystack, -\strlen($needle)) === $needle) {
            return (string) self::substr(
                $haystack,
                0,
                (int) self::strlen($haystack, $encoding) - (int) self::strlen($needle, $encoding),
                $encoding
            );
        }

        return $haystack;
    }

    /**
     * Returns a case swapped version of the string.
     *
     * EXAMPLE: <code>UTF8::swapCase('déJÀ σσς iıII'); // 'DÉjà ΣΣΣ IIii'</code>
     *
     * @param string $str        <p>The input string.</p>
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>Each character's case swapped.</p>
     */
    public static function swapCase(string $str, string $encoding = 'UTF-8', bool $clean_utf8 = false): string
    {
        if ($str === '') {
            return '';
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        if ($encoding === 'UTF-8') {
            return (string) (\mb_strtolower($str) ^ \mb_strtoupper($str) ^ $str);
        }

        return (string) (self::strtolower($str, $encoding) ^ self::strtoupper($str, $encoding) ^ $str);
    }

    /**
     * Checks whether symfony-polyfills are used.
     *
     * @psalm-pure
     *
     * @return bool
     *              <p><strong>true</strong> if in use, <strong>false</strong> otherwise</p>
     *
     * @internal <p>Please do not use it anymore, we will make is private in next major version.</p>
     */
    public static function symfony_polyfill_used(): bool
    {
        // init
        $return = false;

        $return_tmp = \extension_loaded('mbstring');
        if (!$return_tmp && \function_exists('mb_strlen')) {
            $return = true;
        }

        $return_tmp = \extension_loaded('iconv');
        if (!$return_tmp && \function_exists('iconv')) {
            $return = true;
        }

        return $return;
    }

    /**
     * @param string $str
     * @param int    $tab_length
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function tabs_to_spaces(string $str, int $tab_length = 4): string
    {
        if ($tab_length === 4) {
            $spaces = '    ';
        } elseif ($tab_length === 2) {
            $spaces = '  ';
        } else {
            $spaces = \str_repeat(' ', $tab_length);
        }

        return \str_replace("\t", $spaces, $str);
    }

    /**
     * Converts the first character of each word in the string to uppercase
     * and all other chars to lowercase.
     *
     * @param string      $str                           <p>The input string.</p>
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>A string with all characters of $str being title-cased.</p>
     */
    public static function titlecase(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        if (
            $lang === null
            &&
            !$try_to_keep_the_string_length
        ) {
            if ($encoding === 'UTF-8') {
                return \mb_convert_case($str, \MB_CASE_TITLE);
            }

            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            return \mb_convert_case($str, \MB_CASE_TITLE, $encoding);
        }

        return self::str_titleize(
            $str,
            null,
            $encoding,
            false,
            $lang,
            $try_to_keep_the_string_length,
            false
        );
    }

    /**
     * Convert a string into ASCII.
     *
     * EXAMPLE: <code>UTF8::to_ascii('déjà σσς iıii'); // 'deja sss iiii'</code>
     *
     * @param string $str     <p>The input string.</p>
     * @param string $unknown [optional] <p>Character use if character unknown. (default is ?)</p>
     * @param bool   $strict  [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad
     *                        performance</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function to_ascii(
        string $str,
        string $unknown = '?',
        bool $strict = false
    ): string {
        return ASCII::to_transliterate($str, $unknown, $strict);
    }

    /**
     * @param bool|float|int|string $str
     *
     * @psalm-pure
     *
     * @return bool
     */
    public static function to_boolean($str): bool
    {
        // init
        $str = (string) $str;

        if ($str === '') {
            return false;
        }

        // Info: http://php.net/manual/en/filter.filters.validate.php
        $map = [
            'true'  => true,
            '1'     => true,
            'on'    => true,
            'yes'   => true,
            'false' => false,
            '0'     => false,
            'off'   => false,
            'no'    => false,
        ];

        if (isset($map[$str])) {
            return $map[$str];
        }

        $key = \strtolower($str);
        if (isset($map[$key])) {
            return $map[$key];
        }

        if (\is_numeric($str)) {
            return ((float) $str) > 0;
        }

        return (bool) \trim($str);
    }

    /**
     * Convert given string to safe filename (and keep string case).
     *
     * @param string $str
     * @param bool   $use_transliterate No transliteration, conversion etc. is done by default - unsafe characters are
     *                                  simply replaced with hyphen.
     * @param string $fallback_char
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function to_filename(
        string $str,
        bool $use_transliterate = false,
        string $fallback_char = '-'
    ): string {
        return ASCII::to_filename(
            $str,
            $use_transliterate,
            $fallback_char
        );
    }

    /**
     * Convert a string into "ISO-8859"-encoding (Latin-1).
     *
     * EXAMPLE: <code>UTF8::to_utf8(UTF8::to_iso8859('  -ABC-中文空白-  ')); // '  -ABC-????-  '</code>
     *
     * @param string|string[] $str
     *
     * @psalm-pure
     *
     * @return string|string[]
     *
     * @template TToIso8859
     * @phpstan-param TToIso8859 $str
     * @phpstan-return TToIso8859
     */
    public static function to_iso8859($str)
    {
        if (\is_array($str)) {
            foreach ($str as &$v) {
                $v = self::to_iso8859($v);
            }

            return $str;
        }

        $str = (string) $str;
        if ($str === '') {
            return '';
        }

        return self::utf8_decode($str);
    }

    /**
     * This function leaves UTF-8 characters alone, while converting almost all non-UTF8 to UTF8.
     *
     * <ul>
     * <li>It decode UTF-8 codepoints and Unicode escape sequences.</li>
     * <li>It assumes that the encoding of the original string is either WINDOWS-1252 or ISO-8859.</li>
     * <li>WARNING: It does not remove invalid UTF-8 characters, so you maybe need to use "UTF8::clean()" for this
     * case.</li>
     * </ul>
     *
     * EXAMPLE: <code>UTF8::to_utf8(["\u0063\u0061\u0074"]); // array('cat')</code>
     *
     * @param string|string[] $str                        <p>Any string or array of strings.</p>
     * @param bool            $decode_html_entity_to_utf8 <p>Set to true, if you need to decode html-entities.</p>
     *
     * @psalm-pure
     *
     * @return string|string[]
     *                         <p>The UTF-8 encoded string</p>
     *
     * @template TToUtf8
     * @phpstan-param TToUtf8 $str
     * @phpstan-return TToUtf8
     */
    public static function to_utf8($str, bool $decode_html_entity_to_utf8 = false)
    {
        if (\is_array($str)) {
            foreach ($str as &$v) {
                $v = self::to_utf8_string($v, $decode_html_entity_to_utf8);
            }

            /** @phpstan-var TToUtf8 $str */
            return $str;
        }

        /** @phpstan-var TToUtf8 $str */
        $str = self::to_utf8_string($str, $decode_html_entity_to_utf8);

        return $str;
    }

    /**
     * This function leaves UTF-8 characters alone, while converting almost all non-UTF8 to UTF8.
     *
     * <ul>
     * <li>It decode UTF-8 codepoints and Unicode escape sequences.</li>
     * <li>It assumes that the encoding of the original string is either WINDOWS-1252 or ISO-8859.</li>
     * <li>WARNING: It does not remove invalid UTF-8 characters, so you maybe need to use "UTF8::clean()" for this
     * case.</li>
     * </ul>
     *
     * EXAMPLE: <code>UTF8::to_utf8_string("\u0063\u0061\u0074"); // 'cat'</code>
     *
     * @param string $str                        <p>Any string.</p>
     * @param bool   $decode_html_entity_to_utf8 <p>Set to true, if you need to decode html-entities.</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The UTF-8 encoded string</p>
     */
    public static function to_utf8_string(string $str, bool $decode_html_entity_to_utf8 = false): string
    {
        if ($str === '') {
            return $str;
        }

        $max = \strlen($str);
        $buf = '';

        for ($i = 0; $i < $max; ++$i) {
            $c1 = $str[$i];

            if ($c1 >= "\xC0") { // should be converted to UTF8, if it's not UTF8 already

                if ($c1 <= "\xDF") { // looks like 2 bytes UTF8

                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];

                    if ($c2 >= "\x80" && $c2 <= "\xBF") { // yeah, almost sure it's UTF8 already
                        $buf .= $c1 . $c2;
                        ++$i;
                    } else { // not valid UTF8 - convert it
                        $buf .= self::to_utf8_convert_helper($c1);
                    }
                } elseif ($c1 >= "\xE0" && $c1 <= "\xEF") { // looks like 3 bytes UTF8

                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];

                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF") { // yeah, almost sure it's UTF8 already
                        $buf .= $c1 . $c2 . $c3;
                        $i += 2;
                    } else { // not valid UTF8 - convert it
                        $buf .= self::to_utf8_convert_helper($c1);
                    }
                } elseif ($c1 >= "\xF0" && $c1 <= "\xF7") { // looks like 4 bytes UTF8

                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
                    $c4 = $i + 3 >= $max ? "\x00" : $str[$i + 3];

                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF" && $c4 >= "\x80" && $c4 <= "\xBF") { // yeah, almost sure it's UTF8 already
                        $buf .= $c1 . $c2 . $c3 . $c4;
                        $i += 3;
                    } else { // not valid UTF8 - convert it
                        $buf .= self::to_utf8_convert_helper($c1);
                    }
                } else { // doesn't look like UTF8, but should be converted

                    $buf .= self::to_utf8_convert_helper($c1);
                }
            } elseif (($c1 & "\xC0") === "\x80") { // needs conversion

                $buf .= self::to_utf8_convert_helper($c1);
            } else { // it doesn't need conversion

                $buf .= $c1;
            }
        }

        // decode unicode escape sequences + unicode surrogate pairs
        $buf = \preg_replace_callback(
            '/\\\\u([dD][89abAB][0-9a-fA-F]{2})\\\\u([dD][cdefCDEF][\da-fA-F]{2})|\\\\u([0-9a-fA-F]{4})/',
            /**
             * @param array $matches
             *
             * @psalm-pure
             *
             * @return string
             */
            static function (array $matches): string {
                if (isset($matches[3])) {
                    $cp = (int) \hexdec($matches[3]);
                } else {
                    // http://unicode.org/faq/utf_bom.html#utf16-4
                    $cp = ((int) \hexdec($matches[1]) << 10)
                          + (int) \hexdec($matches[2])
                          + 0x10000
                          - (0xD800 << 10)
                          - 0xDC00;
                }

                // https://github.com/php/php-src/blob/php-7.3.2/ext/standard/html.c#L471
                //
                // php_utf32_utf8(unsigned char *buf, unsigned k)

                if ($cp < 0x80) {
                    return (string) self::chr($cp);
                }

                if ($cp < 0xA0) {
                    /** @noinspection UnnecessaryCastingInspection */
                    return (string) self::chr(0xC0 | $cp >> 6) . (string) self::chr(0x80 | $cp & 0x3F);
                }

                return self::decimal_to_chr($cp);
            },
            $buf
        );

        if ($buf === null) {
            return '';
        }

        // decode UTF-8 codepoints
        if ($decode_html_entity_to_utf8) {
            $buf = self::html_entity_decode($buf);
        }

        return $buf;
    }

    /**
     * Returns the given string as an integer, or null if the string isn't numeric.
     *
     * @param string $str
     *
     * @psalm-pure
     *
     * @return int|null
     *                  <p>null if the string isn't numeric</p>
     */
    public static function to_int(string $str)
    {
        if (\is_numeric($str)) {
            return (int) $str;
        }

        return null;
    }

    /**
     * Returns the given input as string, or null if the input isn't int|float|string
     * and do not implement the "__toString()" method.
     *
     * @param float|int|object|string|null $input
     *
     * @psalm-pure
     *
     * @return string|null
     *                     <p>null if the input isn't int|float|string and has no "__toString()" method</p>
     */
    public static function to_string($input)
    {
        if ($input === null) {
            return null;
        }

        /** @var string $input_type - hack for psalm */
        $input_type = \gettype($input);

        if (
            $input_type === 'string'
            ||
            $input_type === 'integer'
            ||
            $input_type === 'float'
            ||
            $input_type === 'double'
        ) {
            return (string) $input;
        }

        /** @phpstan-ignore-next-line - "gettype": FP? */
        if ($input_type === 'object' && \method_exists($input, '__toString')) {
            return (string) $input;
        }

        return null;
    }

    /**
     * Strip whitespace or other characters from the beginning and end of a UTF-8 string.
     *
     * INFO: This is slower then "trim()"
     *
     * We can only use the original-function, if we use <= 7-Bit in the string / chars
     * but the check for ASCII (7-Bit) cost more time, then we can safe here.
     *
     * EXAMPLE: <code>UTF8::trim('   -ABC-中文空白-  '); // '-ABC-中文空白-'</code>
     *
     * @param string      $str   <p>The string to be trimmed</p>
     * @param string|null $chars [optional] <p>Optional characters to be stripped</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The trimmed string.</p>
     */
    public static function trim(string $str = '', string $chars = null): string
    {
        if ($str === '') {
            return '';
        }

        if (self::$SUPPORT['mbstring'] === true) {
            if ($chars !== null) {
                /** @noinspection PregQuoteUsageInspection */
                $chars = \preg_quote($chars);
                $pattern = "^[${chars}]+|[${chars}]+\$";
            } else {
                $pattern = '^[\\s]+|[\\s]+$';
            }

            return (string) \mb_ereg_replace($pattern, '', $str);
        }

        if ($chars !== null) {
            $chars = \preg_quote($chars, '/');
            $pattern = "^[${chars}]+|[${chars}]+\$";
        } else {
            $pattern = '^[\\s]+|[\\s]+$';
        }

        return self::regex_replace($str, $pattern, '');
    }

    /**
     * Makes string's first char uppercase.
     *
     * EXAMPLE: <code>UTF8::ucfirst('ñtërnâtiônàlizætiøn foo'); // 'Ñtërnâtiônàlizætiøn foo'</code>
     *
     * @param string      $str                           <p>The input string.</p>
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
     *                                                   tr</p>
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
     *                                                   -> ß</p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The resulting string with with char uppercase.</p>
     */
    public static function ucfirst(
        string $str,
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false,
        string $lang = null,
        bool $try_to_keep_the_string_length = false
    ): string {
        if ($str === '') {
            return '';
        }

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;

        if ($encoding === 'UTF-8') {
            $str_part_two = (string) \mb_substr($str, 1);

            if ($use_mb_functions) {
                $str_part_one = \mb_strtoupper(
                    (string) \mb_substr($str, 0, 1)
                );
            } else {
                $str_part_one = self::strtoupper(
                    (string) \mb_substr($str, 0, 1),
                    $encoding,
                    false,
                    $lang,
                    $try_to_keep_the_string_length
                );
            }
        } else {
            $encoding = self::normalize_encoding($encoding, 'UTF-8');

            $str_part_two = (string) self::substr($str, 1, null, $encoding);

            if ($use_mb_functions) {
                $str_part_one = \mb_strtoupper(
                    (string) \mb_substr($str, 0, 1, $encoding),
                    $encoding
                );
            } else {
                $str_part_one = self::strtoupper(
                    (string) self::substr($str, 0, 1, $encoding),
                    $encoding,
                    false,
                    $lang,
                    $try_to_keep_the_string_length
                );
            }
        }

        return $str_part_one . $str_part_two;
    }

    /**
     * Uppercase for all words in the string.
     *
     * EXAMPLE: <code>UTF8::ucwords('iñt ërn âTi ônà liz æti øn'); // 'Iñt Ërn ÂTi Ônà Liz Æti Øn'</code>
     *
     * @param string   $str        <p>The input string.</p>
     * @param string[] $exceptions [optional] <p>Exclusion for some words.</p>
     * @param string   $char_list  [optional] <p>Additional chars that contains to words and do not start a new
     *                             word.</p>
     * @param string   $encoding   [optional] <p>Set the charset.</p>
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function ucwords(
        string $str,
        array $exceptions = [],
        string $char_list = '',
        string $encoding = 'UTF-8',
        bool $clean_utf8 = false
    ): string {
        if (!$str) {
            return '';
        }

        // INFO: mb_convert_case($str, MB_CASE_TITLE);
        // -> MB_CASE_TITLE didn't only uppercase the first letter, it also lowercase all other letters

        if ($clean_utf8) {
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
            // if invalid characters are found in $haystack before $needle
            $str = self::clean($str);
        }

        $use_php_default_functions = !(bool) ($char_list . \implode('', $exceptions));

        if (
            $use_php_default_functions
            &&
            ASCII::is_ascii($str)
        ) {
            return \ucwords($str);
        }

        $words = self::str_to_words($str, $char_list);
        $use_exceptions = $exceptions !== [];

        $words_str = '';
        foreach ($words as &$word) {
            if (!$word) {
                continue;
            }

            if (
                !$use_exceptions
                ||
                !\in_array($word, $exceptions, true)
            ) {
                $words_str .= self::ucfirst($word, $encoding);
            } else {
                $words_str .= $word;
            }
        }

        return $words_str;
    }

    /**
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
     *
     * EXAMPLE: <code>UTF8::urldecode('tes%20öäü%20\u00edtest+test'); // 'tes öäü ítest test'</code>
     *
     * e.g:
     * 'test+test'                     => 'test test'
     * 'D&#252;sseldorf'               => 'Düsseldorf'
     * 'D%FCsseldorf'                  => 'Düsseldorf'
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
     * 'Düsseldorf'                   => 'Düsseldorf'
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
     *
     * @param string $str          <p>The input string.</p>
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function urldecode(string $str, bool $multi_decode = true): string
    {
        if ($str === '') {
            return '';
        }

        $str = self::urldecode_unicode_helper($str);

        if ($multi_decode) {
            do {
                $str_compare = $str;

                /**
                 * @psalm-suppress PossiblyInvalidArgument
                 */
                $str = \urldecode(
                    self::html_entity_decode(
                        self::to_utf8($str),
                        \ENT_QUOTES | \ENT_HTML5
                    )
                );
            } while ($str_compare !== $str);
        } else {
            /**
             * @psalm-suppress PossiblyInvalidArgument
             */
            $str = \urldecode(
                self::html_entity_decode(
                    self::to_utf8($str),
                    \ENT_QUOTES | \ENT_HTML5
                )
            );
        }

        return self::fix_simple_utf8($str);
    }

    /**
     * Decodes a UTF-8 string to ISO-8859-1.
     *
     * EXAMPLE: <code>UTF8::encode('UTF-8', UTF8::utf8_decode('-ABC-中文空白-')); // '-ABC-????-'</code>
     *
     * @param string $str             <p>The input string.</p>
     * @param bool   $keep_utf8_chars
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function utf8_decode(string $str, bool $keep_utf8_chars = false): string
    {
        if ($str === '') {
            return '';
        }

        // save for later comparision
        $str_backup = $str;
        $len = \strlen($str);

        if (self::$ORD === null) {
            self::$ORD = self::getData('ord');
        }

        if (self::$CHR === null) {
            self::$CHR = self::getData('chr');
        }

        $no_char_found = '?';
        for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
            switch ($str[$i] & "\xF0") {
                case "\xC0":
                case "\xD0":
                    $c = (self::$ORD[$str[$i] & "\x1F"] << 6) | self::$ORD[$str[++$i] & "\x3F"];
                    $str[$j] = $c < 256 ? self::$CHR[$c] : $no_char_found;

                    break;

                case "\xF0":
                    ++$i;

                // no break

                case "\xE0":
                    $str[$j] = $no_char_found;
                    $i += 2;

                    break;

                default:
                    $str[$j] = $str[$i];
            }
        }

        /** @var false|string $return - needed for PhpStan (stubs error) */
        $return = \substr($str, 0, $j);
        if ($return === false) {
            $return = '';
        }

        if (
            $keep_utf8_chars
            &&
            (int) self::strlen($return) >= (int) self::strlen($str_backup)
        ) {
            return $str_backup;
        }

        return $return;
    }

    /**
     * Encodes an ISO-8859-1 string to UTF-8.
     *
     * EXAMPLE: <code>UTF8::utf8_decode(UTF8::utf8_encode('-ABC-中文空白-')); // '-ABC-中文空白-'</code>
     *
     * @param string $str <p>The input string.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function utf8_encode(string $str): string
    {
        if ($str === '') {
            return '';
        }

        /** @var false|string $str - the polyfill maybe return false */
        $str = \utf8_encode($str);

        if ($str === false) {
            return '';
        }

        return $str;
    }

    /**
     * Returns an array with all utf8 whitespace characters.
     *
     * @see http://www.bogofilter.org/pipermail/bogofilter/2003-March/001889.html
     *
     * @psalm-pure
     *
     * @return string[]
     *                  An array with all known whitespace characters as values and the type of whitespace as keys
     *                  as defined in above URL
     */
    public static function whitespace_table(): array
    {
        return self::$WHITESPACE_TABLE;
    }

    /**
     * Limit the number of words in a string.
     *
     * EXAMPLE: <code>UTF8::words_limit('fòô bàř fòô', 2, ''); // 'fòô bàř'</code>
     *
     * @param string $str        <p>The input string.</p>
     * @param int    $limit      <p>The limit of words as integer.</p>
     * @param string $str_add_on <p>Replacement for the striped string.</p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function words_limit(
        string $str,
        int $limit = 100,
        string $str_add_on = '…'
    ): string {
        if ($str === '' || $limit < 1) {
            return '';
        }

        \preg_match('/^\\s*+(?:[^\\s]++\\s*+){1,' . $limit . '}/u', $str, $matches);

        if (
            !isset($matches[0])
            ||
            \mb_strlen($str) === (int) \mb_strlen($matches[0])
        ) {
            return $str;
        }

        return \rtrim($matches[0]) . $str_add_on;
    }

    /**
     * Wraps a string to a given number of characters
     *
     * EXAMPLE: <code>UTF8::wordwrap('Iñtërnâtiônàlizætiøn', 2, '<br>', true)); // 'Iñ<br>të<br>rn<br>ât<br>iô<br>nà<br>li<br>zæ<br>ti<br>øn'</code>
     *
     * @see http://php.net/manual/en/function.wordwrap.php
     *
     * @param string $str   <p>The input string.</p>
     * @param int    $width [optional] <p>The column width.</p>
     * @param string $break [optional] <p>The line is broken using the optional break parameter.</p>
     * @param bool   $cut   [optional] <p>
     *                      If the cut is set to true, the string is
     *                      always wrapped at or before the specified width. So if you have
     *                      a word that is larger than the given width, it is broken apart.
     *                      </p>
     *
     * @psalm-pure
     *
     * @return string
     *                <p>The given string wrapped at the specified column.</p>
     */
    public static function wordwrap(
        string $str,
        int $width = 75,
        string $break = "\n",
        bool $cut = false
    ): string {
        if ($str === '' || $break === '') {
            return '';
        }

        $str_split = \explode($break, $str);

        /** @var string[] $charsArray */
        $charsArray = [];
        $word_split = '';
        foreach ($str_split as $i => $i_value) {
            if ($i) {
                $charsArray[] = $break;
                $word_split .= '#';
            }

            foreach (self::str_split($i_value) as $c) {
                $charsArray[] = $c;
                if ($c === ' ') {
                    $word_split .= ' ';
                } else {
                    $word_split .= '?';
                }
            }
        }

        $str_return = '';
        $j = 0;
        $b = -1;
        $i = -1;
        $word_split = \wordwrap($word_split, $width, '#', $cut);

        $max = \mb_strlen($word_split);
        /** @noinspection PhpAssignmentInConditionInspection - is ok here */
        while (($b = \mb_strpos($word_split, '#', $b + 1)) !== false) {
            for (++$i; $i < $b; ++$i) {
                if (isset($charsArray[$j])) {
                    $str_return .= $charsArray[$j];
                    unset($charsArray[$j]);
                }
                ++$j;

                // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
                if ($i > $max) {
                    break 2;
                }
            }

            if (
                $break === $charsArray[$j]
                ||
                $charsArray[$j] === ' '
            ) {
                unset($charsArray[$j++]);
            }

            $str_return .= $break;

            // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
            if ($b > $max) {
                break;
            }
        }

        return $str_return . \implode('', $charsArray);
    }

    /**
     * Line-Wrap the string after $limit, but split the string by "$delimiter" before ...
     *    ... so that we wrap the per line.
     *
     * @param string      $str             <p>The input string.</p>
     * @param int         $width           [optional] <p>The column width.</p>
     * @param string      $break           [optional] <p>The line is broken using the optional break parameter.</p>
     * @param bool        $cut             [optional] <p>
     *                                     If the cut is set to true, the string is
     *                                     always wrapped at or before the specified width. So if you have
     *                                     a word that is larger than the given width, it is broken apart.
     *                                     </p>
     * @param bool        $add_final_break [optional] <p>
     *                                     If this flag is true, then the method will add a $break at the end
     *                                     of the result string.
     *                                     </p>
     * @param string|null $delimiter       [optional] <p>
     *                                     You can change the default behavior, where we split the string by newline.
     *                                     </p>
     *
     * @psalm-pure
     *
     * @return string
     */
    public static function wordwrap_per_line(
        string $str,
        int $width = 75,
        string $break = "\n",
        bool $cut = false,
        bool $add_final_break = true,
        string $delimiter = null
    ): string {
        if ($delimiter === null) {
            $strings = \preg_split('/\\r\\n|\\r|\\n/', $str);
        } else {
            $strings = \explode($delimiter, $str);
        }

        $string_helper_array = [];
        if ($strings !== false) {
            foreach ($strings as $value) {
                $string_helper_array[] = self::wordwrap($value, $width, $break, $cut);
            }
        }

        if ($add_final_break) {
            $final_break = $break;
        } else {
            $final_break = '';
        }

        return \implode($delimiter ?? "\n", $string_helper_array) . $final_break;
    }

    /**
     * Returns an array of Unicode White Space characters.
     *
     * @psalm-pure
     *
     * @return string[]
     *                  <p>An array with numeric code point as key and White Space Character as value.</p>
     */
    public static function ws(): array
    {
        return self::$WHITESPACE;
    }

    /**
     * Checks whether the passed string contains only byte sequences that are valid UTF-8 characters.
     *
     * EXAMPLE: <code>
     * UTF8::is_utf8_string('Iñtërnâtiônàlizætiøn']); // true
     * //
     * UTF8::is_utf8_string("Iñtërnâtiônàlizætiøn\xA0\xA1"); // false
     * </code>
     *
     * @see          http://hsivonen.iki.fi/php-utf8/
     *
     * @param string $str    <p>The string to be checked.</p>
     * @param bool   $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
     *
     * @psalm-pure
     *
     * @return bool
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function is_utf8_string(string $str, bool $strict = false)
    {
        if ($str === '') {
            return true;
        }

        if ($strict) {
            $is_binary = self::is_binary($str, true);

            if ($is_binary && self::is_utf16($str, false) !== false) {
                return false;
            }

            if ($is_binary && self::is_utf32($str, false) !== false) {
                return false;
            }
        }

        if (self::$SUPPORT['pcre_utf8']) {
            // If even just the first character can be matched, when the /u
            // modifier is used, then it's valid UTF-8. If the UTF-8 is somehow
            // invalid, nothing at all will match, even if the string contains
            // some valid sequences
            return \preg_match('/^./us', $str) === 1;
        }

        $mState = 0; // cached expected number of octets after the current octet
        // until the beginning of the next UTF8 character sequence
        $mUcs4 = 0; // cached Unicode character
        $mBytes = 1; // cached expected number of octets in the current sequence

        if (self::$ORD === null) {
            self::$ORD = self::getData('ord');
        }

        $len = \strlen($str);
        for ($i = 0; $i < $len; ++$i) {
            $in = self::$ORD[$str[$i]];

            if ($mState === 0) {
                // When mState is zero we expect either a US-ASCII character or a
                // multi-octet sequence.
                if ((0x80 & $in) === 0) {
                    // US-ASCII, pass straight through.
                    $mBytes = 1;
                } elseif ((0xE0 & $in) === 0xC0) {
                    // First octet of 2 octet sequence.
                    $mUcs4 = $in;
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
                    $mState = 1;
                    $mBytes = 2;
                } elseif ((0xF0 & $in) === 0xE0) {
                    // First octet of 3 octet sequence.
                    $mUcs4 = $in;
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
                    $mState = 2;
                    $mBytes = 3;
                } elseif ((0xF8 & $in) === 0xF0) {
                    // First octet of 4 octet sequence.
                    $mUcs4 = $in;
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
                    $mState = 3;
                    $mBytes = 4;
                } elseif ((0xFC & $in) === 0xF8) {
                    /* First octet of 5 octet sequence.
                     *
                     * This is illegal because the encoded codepoint must be either
                     * (a) not the shortest form or
                     * (b) outside the Unicode range of 0-0x10FFFF.
                     * Rather than trying to resynchronize, we will carry on until the end
                     * of the sequence and let the later error handling code catch it.
                     */
                    $mUcs4 = $in;
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
                    $mState = 4;
                    $mBytes = 5;
                } elseif ((0xFE & $in) === 0xFC) {
                    // First octet of 6 octet sequence, see comments for 5 octet sequence.
                    $mUcs4 = $in;
                    $mUcs4 = ($mUcs4 & 1) << 30;
                    $mState = 5;
                    $mBytes = 6;
                } else {
                    // Current octet is neither in the US-ASCII range nor a legal first
                    // octet of a multi-octet sequence.
                    return false;
                }
            } elseif ((0xC0 & $in) === 0x80) {

                // When mState is non-zero, we expect a continuation of the multi-octet
                // sequence

                // Legal continuation.
                $shift = ($mState - 1) * 6;
                $tmp = $in;
                $tmp = ($tmp & 0x0000003F) << $shift;
                $mUcs4 |= $tmp;
                // Prefix: End of the multi-octet sequence. mUcs4 now contains the final
                // Unicode code point to be output.
                if (--$mState === 0) {
                    // Check for illegal sequences and code points.
                    //
                    // From Unicode 3.1, non-shortest form is illegal
                    if (
                        ($mBytes === 2 && $mUcs4 < 0x0080)
                        ||
                        ($mBytes === 3 && $mUcs4 < 0x0800)
                        ||
                        ($mBytes === 4 && $mUcs4 < 0x10000)
                        ||
                        ($mBytes > 4)
                        ||
                        // From Unicode 3.2, surrogate characters are illegal.
                        (($mUcs4 & 0xFFFFF800) === 0xD800)
                        ||
                        // Code points outside the Unicode range are illegal.
                        ($mUcs4 > 0x10FFFF)
                    ) {
                        return false;
                    }
                    // initialize UTF8 cache
                    $mState = 0;
                    $mUcs4 = 0;
                    $mBytes = 1;
                }
            } else {
                // ((0xC0 & (*in) != 0x80) && (mState != 0))
                // Incomplete multi-octet sequence.
                return false;
            }
        }

        return $mState === 0;
    }

    /**
     * @param string $str
     * @param bool   $use_lowercase      <p>Use uppercase by default, otherwise use lowercase.</p>
     * @param bool   $use_full_case_fold <p>Convert not only common cases.</p>
     *
     * @psalm-pure
     *
     * @return string
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function fixStrCaseHelper(
        string $str,
        bool $use_lowercase = false,
        bool $use_full_case_fold = false
    ) {
        $upper = self::$COMMON_CASE_FOLD['upper'];
        $lower = self::$COMMON_CASE_FOLD['lower'];

        if ($use_lowercase) {
            $str = \str_replace(
                $upper,
                $lower,
                $str
            );
        } else {
            $str = \str_replace(
                $lower,
                $upper,
                $str
            );
        }

        if ($use_full_case_fold) {
            /**
             * @psalm-suppress ImpureStaticVariable
             *
             * @var array<mixed>|null
             */
            static $FULL_CASE_FOLD = null;
            if ($FULL_CASE_FOLD === null) {
                $FULL_CASE_FOLD = self::getData('caseFolding_full');
            }

            if ($use_lowercase) {
                $str = \str_replace($FULL_CASE_FOLD[0], $FULL_CASE_FOLD[1], $str);
            } else {
                $str = \str_replace($FULL_CASE_FOLD[1], $FULL_CASE_FOLD[0], $str);
            }
        }

        return $str;
    }

    /**
     * get data from "/data/*.php"
     *
     * @param string $file
     *
     * @psalm-pure
     *
     * @return array
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function getData(string $file)
    {
        /** @noinspection PhpIncludeInspection */
        /** @noinspection UsingInclusionReturnValueInspection */
        /** @psalm-suppress UnresolvableInclude */
        return include __DIR__ . '/data/' . $file . '.php';
    }

    /**
     * @psalm-pure
     *
     * @return true|null
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function initEmojiData()
    {
        if (self::$EMOJI_KEYS_CACHE === null) {
            if (self::$EMOJI === null) {
                self::$EMOJI = self::getData('emoji');
            }

            /**
             * @psalm-suppress ImpureFunctionCall - static sort function is used
             */
            \uksort(
                self::$EMOJI,
                static function (string $a, string $b): int {
                    return \strlen($b) <=> \strlen($a);
                }
            );

            self::$EMOJI_KEYS_CACHE = \array_keys(self::$EMOJI);
            self::$EMOJI_VALUES_CACHE = self::$EMOJI;

            foreach (self::$EMOJI_KEYS_CACHE as $key) {
                $tmp_key = \crc32($key);
                self::$EMOJI_KEYS_REVERSIBLE_CACHE[] = '_-_PORTABLE_UTF8_-_' . $tmp_key . '_-_' . \strrev((string) $tmp_key) . '_-_8FTU_ELBATROP_-_';
            }

            return true;
        }

        return null;
    }

    /**
     * Checks whether mbstring "overloaded" is active on the server.
     *
     * @psalm-pure
     *
     * @return bool
     */
    private static function mbstring_overloaded(): bool
    {
        /**
         * INI directive 'mbstring.func_overload' is deprecated since PHP 7.2
         */

        /** @noinspection PhpComposerExtensionStubsInspection */
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
        /** @noinspection DeprecatedIniOptionsInspection */
        return \defined('MB_OVERLOAD_STRING')
               &&
               ((int) @\ini_get('mbstring.func_overload') & \MB_OVERLOAD_STRING);
    }

    /**
     * @param array    $strings
     * @param bool     $remove_empty_values
     * @param int|null $remove_short_values
     *
     * @psalm-pure
     *
     * @return array
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function reduce_string_array(
        array $strings,
        bool $remove_empty_values,
        int $remove_short_values = null
    ) {
        // init
        $return = [];

        foreach ($strings as &$str) {
            if (
                $remove_short_values !== null
                &&
                \mb_strlen($str) <= $remove_short_values
            ) {
                continue;
            }

            if (
                $remove_empty_values
                &&
                \trim($str) === ''
            ) {
                continue;
            }

            $return[] = $str;
        }

        return $return;
    }

    /**
     * rxClass
     *
     * @param string $s
     * @param string $class
     *
     * @return string
     *                    *
     * @psalm-pure
     */
    private static function rxClass(string $s, string $class = '')
    {
        /**
         * @psalm-suppress ImpureStaticVariable
         *
         * @var array<string,string>
         */
        static $RX_CLASS_CACHE = [];

        $cache_key = $s . '_' . $class;

        if (isset($RX_CLASS_CACHE[$cache_key])) {
            return $RX_CLASS_CACHE[$cache_key];
        }

        $class_array[] = $class;

        /** @noinspection SuspiciousLoopInspection */
        /** @noinspection AlterInForeachInspection */
        foreach (self::str_split($s) as &$s) {
            if ($s === '-') {
                $class_array[0] = '-' . $class_array[0];
            } elseif (!isset($s[2])) {
                $class_array[0] .= \preg_quote($s, '/');
            } elseif (self::strlen($s) === 1) {
                $class_array[0] .= $s;
            } else {
                $class_array[] = $s;
            }
        }

        if ($class_array[0]) {
            $class_array[0] = '[' . $class_array[0] . ']';
        }

        if (\count($class_array) === 1) {
            $return = $class_array[0];
        } else {
            $return = '(?:' . \implode('|', $class_array) . ')';
        }

        $RX_CLASS_CACHE[$cache_key] = $return;

        return $return;
    }

    /**
     * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
     *
     * @param string $names
     * @param string $delimiter
     * @param string $encoding
     *
     * @psalm-pure
     *
     * @return string
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function str_capitalize_name_helper(
        string $names,
        string $delimiter,
        string $encoding = 'UTF-8'
    ) {
        // init
        $name_helper_array = \explode($delimiter, $names);
        if ($name_helper_array === false) {
            return '';
        }

        $special_cases = [
            'names' => [
                'ab',
                'af',
                'al',
                'and',
                'ap',
                'bint',
                'binte',
                'da',
                'de',
                'del',
                'den',
                'der',
                'di',
                'dit',
                'ibn',
                'la',
                'mac',
                'nic',
                'of',
                'ter',
                'the',
                'und',
                'van',
                'von',
                'y',
                'zu',
            ],
            'prefixes' => [
                'al-',
                "d'",
                'ff',
                "l'",
                'mac',
                'mc',
                'nic',
            ],
        ];

        foreach ($name_helper_array as &$name) {
            if (\in_array($name, $special_cases['names'], true)) {
                continue;
            }

            $continue = false;

            if ($delimiter === '-') {
                foreach ((array) $special_cases['names'] as &$beginning) {
                    if (\strncmp($name, $beginning, \strlen($beginning)) === 0) {
                        $continue = true;

                        break;
                    }
                }
                unset($beginning);
            }

            foreach ((array) $special_cases['prefixes'] as &$beginning) {
                if (\strncmp($name, $beginning, \strlen($beginning)) === 0) {
                    $continue = true;

                    break;
                }
            }
            unset($beginning);

            if ($continue) {
                continue;
            }

            $name = self::ucfirst($name, $encoding);
        }

        return \implode($delimiter, $name_helper_array);
    }

    /**
     * Generic case-sensitive transformation for collation matching.
     *
     * @param string $str <p>The input string</p>
     *
     * @psalm-pure
     *
     * @return string|null
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function strtonatfold(string $str)
    {
        $str = \Normalizer::normalize($str, \Normalizer::NFD);
        if ($str === false) {
            return '';
        }

        return \preg_replace(
            '/\p{Mn}+/u',
            '',
            $str
        );
    }

    /**
     * @param int|string $input
     *
     * @psalm-pure
     *
     * @return string
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function to_utf8_convert_helper($input)
    {
        // init
        $buf = '';

        if (self::$ORD === null) {
            self::$ORD = self::getData('ord');
        }

        if (self::$CHR === null) {
            self::$CHR = self::getData('chr');
        }

        if (self::$WIN1252_TO_UTF8 === null) {
            self::$WIN1252_TO_UTF8 = self::getData('win1252_to_utf8');
        }

        $ordC1 = self::$ORD[$input];
        if (isset(self::$WIN1252_TO_UTF8[$ordC1])) { // found in Windows-1252 special cases
            $buf .= self::$WIN1252_TO_UTF8[$ordC1];
        } else {
            /** @noinspection OffsetOperationsInspection */
            $cc1 = self::$CHR[$ordC1 / 64] | "\xC0";
            $cc2 = ((string) $input & "\x3F") | "\x80";
            $buf .= $cc1 . $cc2;
        }

        return $buf;
    }

    /**
     * @param string $str
     *
     * @psalm-pure
     *
     * @return string
     *
     * @noinspection ReturnTypeCanBeDeclaredInspection
     */
    private static function urldecode_unicode_helper(string $str)
    {
        if (\strpos($str, '%u') === false) {
            return $str;
        }

        $pattern = '/%u([0-9a-fA-F]{3,4})/';
        if (\preg_match($pattern, $str)) {
            $str = (string) \preg_replace($pattern, '&#x\\1;', $str);
        }

        return $str;
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit