<commit_before>﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Pchp.Core;
using Pchp.Core.Reflection;
using static Pchp.Library.StandardPhpOptions;

namespace Pchp.Library
{
    [PhpExtension(ExtensionName, Registrator = typeof(Registrator))]
    public static class MultiByteString
    {
        const string ExtensionName = "mbstring";

        #region MbConfig, Options

        sealed class MbConfig : IPhpConfiguration
        {
            public Encoding InternalEncoding { get; set; }

            public Encoding RegexEncoding { get; set; }

            public Encoding HttpOutputEncoding { get; set; }

            public IPhpConfiguration Copy() => (MbConfig)this.MemberwiseClone();

            public string ExtensionName => MultiByteString.ExtensionName;

            public Encoding[] DetectOrder { get; set; } = new[] { Encoding.ASCII, Encoding.UTF8 };

            /// <summary>
            /// Gets or sets a value of a legacy configuration option.
            /// </summary>
            private static PhpValue GetSet(Context ctx, IPhpConfigurationService config, string option, PhpValue value, IniAction action)
            {
                var local = config.Get<MbConfig>();
                if (local == null)
                {
                    return PhpValue.False;
                }

                switch (option)
                {
                    //mbstring.language   "neutral"   PHP_INI_ALL Available since PHP 4.3.0.PHP_INI_PERDIR in PHP <= 5.2.6
                    //mbstring.detect_order NULL    PHP_INI_ALL Available since PHP 4.0.6.
                    //mbstring.http_input "pass"  PHP_INI_ALL Available since PHP 4.0.6.Deprecated in PHP 5.6.0.
                    //mbstring.http_output    "pass"  PHP_INI_ALL Available since PHP 4.0.6.Deprecated in PHP 5.6.0.
                    //mbstring.script_encoding    NULL PHP_INI_ALL Available since PHP 4.3.0.Removed in PHP 5.4.0.Use zend.script_encoding instead.
                    //mbstring.substitute_character NULL    PHP_INI_ALL Available since PHP 4.0.6.

                    case "mbstring.func_overload":
                        // "0" PHP_INI_SYSTEM PHP_INI_PERDIR from PHP 4.3 to 5.2.6, otherwise PHP_INI_SYSTEM. Available since PHP 4.2.0.Deprecated in PHP 7.2.0.
                        return (PhpValue)0;

                    //mbstring.encoding_translation   "0" PHP_INI_PERDIR Available since PHP 4.3.0.
                    //mbstring.strict_detection   "0" PHP_INI_ALL Available since PHP 5.1.2.
                    //mbstring.internal_encoding  NULL PHP_INI_ALL Available since PHP 4.0.6.Deprecated in PHP 5.6.0.

                    default:
                        break;
                }

                Debug.Fail("Option '" + option + "' is not currently supported.");
                return PhpValue.False;
            }

            /// <summary>
            /// Registers legacy ini-options.
            /// </summary>
            internal static void RegisterLegacyOptions()
            {
                var d = new GetSetDelegate(GetSet);

                //Register("mbstring.internal_encoding", IniFlags.Supported | IniFlags.Local, d, s);
                Register("mbstring.func_overload", IniFlags.Supported | IniFlags.Local, d, MultiByteString.ExtensionName);
            }
        }

        static MbConfig GetConfig(Context ctx) => ctx.Configuration.Get<MbConfig>();

        internal class Registrator
        {
            public Registrator()
            {
                Context.RegisterConfiguration(new MbConfig());
                MbConfig.RegisterLegacyOptions();
                Encoding.RegisterProvider(new PhpEncodingProvider());
            }
        }

        #endregion

        #region Constants

        [Flags, PhpHidden]
        public enum OverloadConstants
        {
            MB_OVERLOAD_MAIL = 1,

            MB_OVERLOAD_STRING = 2,

            MB_OVERLOAD_REGEX = 4,
        }

        public const int MB_OVERLOAD_MAIL = (int)OverloadConstants.MB_OVERLOAD_MAIL;
        public const int MB_OVERLOAD_STRING = (int)OverloadConstants.MB_OVERLOAD_STRING;
        public const int MB_OVERLOAD_REGEX = (int)OverloadConstants.MB_OVERLOAD_REGEX;

        [Flags, PhpHidden]
        public enum CaseConstants
        {
            MB_CASE_UPPER = 0,

            MB_CASE_LOWER = 1,

            MB_CASE_TITLE = 2,

            MB_CASE_FOLD = 3,

            MB_CASE_UPPER_SIMPLE = 4,

            MB_CASE_LOWER_SIMPLE = 5,

            MB_CASE_TITLE_SIMPLE = 6,

            MB_CASE_FOLD_SIMPLE = 7,
        }

        public const int MB_CASE_UPPER = (int)CaseConstants.MB_CASE_UPPER;
        public const int MB_CASE_LOWER = (int)CaseConstants.MB_CASE_LOWER;
        public const int MB_CASE_TITLE = (int)CaseConstants.MB_CASE_TITLE;
        public const int MB_CASE_FOLD = (int)CaseConstants.MB_CASE_FOLD;
        public const int MB_CASE_UPPER_SIMPLE = (int)CaseConstants.MB_CASE_UPPER_SIMPLE;
        public const int MB_CASE_LOWER_SIMPLE = (int)CaseConstants.MB_CASE_LOWER_SIMPLE;
        public const int MB_CASE_TITLE_SIMPLE = (int)CaseConstants.MB_CASE_TITLE_SIMPLE;
        public const int MB_CASE_FOLD_SIMPLE = (int)CaseConstants.MB_CASE_FOLD_SIMPLE;

        #endregion

        #region Encodings

        /// <summary>
        /// Encoding name.
        /// Provides case insensitive comparison.
        /// </summary>
        readonly struct EncodingName : IEquatable<EncodingName>, IEquatable<string>
        {
            public string Name { get; }

            static readonly HashSet<string> s_unicodenames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "utf8", "utf-8", "utf16", "utf-16", "utf-16le", "utf-16be", "unicode", "utf",
            };

            public EncodingName(string name)
            {
                Name = name;
            }

            public static implicit operator EncodingName(string name) => new EncodingName(name);

            public bool IsCodePageEncoding(out int codepage) => IsCodePageEncoding(Name, out codepage);

            public static bool IsCodePageEncoding(string name, out int codepage)
            {
                if (name != null)
                {
                    // cp{CodePage}
                    if (name.StartsWith("cp", StringComparison.OrdinalIgnoreCase) &&
                        name.Length > 2 &&
                        int.TryParse(name.Substring(2), out codepage)) // TODO: netstandard2.1 ReadOnlySpan
                    {
                        return true;
                    }

                    // Codepage - {CodePage}
                    const string CodepagePrefix = "Codepage - ";
                    if (name.StartsWith(CodepagePrefix, StringComparison.OrdinalIgnoreCase) &&
                        name.Length > CodepagePrefix.Length &&
                        int.TryParse(name.Substring(CodepagePrefix.Length), out codepage))  // TODO: netstandard2.1 ReadOnlySpan
                    {
                        return true;
                    }
                }

                //
                codepage = 0;
                return false;
            }

            public bool Is8bit() => Equals("8bit");

            public bool IsUtfEncoding() => IsUtfEncoding(Name);

            public static bool IsUtfEncoding(string name) => name != null && s_unicodenames.Contains(name);

            static bool Equals(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);

            public bool Equals(EncodingName other) => Equals(Name, other.Name);

            public bool Equals(string other) => Equals(Name, other);

            public override bool Equals(object obj)
                => obj is EncodingName encname
                    ? Equals(encname)
                    : obj is string str && Equals(new EncodingName(str));

            public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name ?? "");

        }

        sealed class PhpEncodingProvider : EncodingProvider
        {
            static readonly Dictionary<EncodingName, Func<Encoding>> s_encmap = new Dictionary<EncodingName, Func<Encoding>>()
            {
                //enc["pass"] = Encoding.Default; // TODO: "pass" encoding
                { "auto", () => Encoding.UTF8 },
                { "wchar", () => Encoding.Unicode },
                //byte2be
                //byte2le
                //byte4be
                //byte4le
                //BASE64
                //UUENCODE
                //HTML-ENTITIES
                //Quoted-Printable
                //7bit
                //8bit // EightBitEncoding.Instance
                //UCS-4
                //UCS-4BE
                //UCS-4LE
                //UCS-2
                //UCS-2BE
                //UCS-2LE
                //UTF-32
                //UTF-32BE
                //UTF-32LE
                //UTF-16
                //UTF-16BE
                { "UTF-16LE", () => Encoding.Unicode }, // alias UTF-16
                { "UTF-8", () => Encoding.UTF8 },       // alias UTF8
                //UTF-7
                //UTF7-IMAP
                { "ASCII", () => Encoding.ASCII },      // alias us-ascii
                //EUC-JP
                //SJIS
                //eucJP-win
                //SJIS-win
                //CP51932
                //JIS
                //ISO-2022-JP
                //ISO-2022-JP-MS
                //Windows-1252
                //Windows-1254
                //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
                //EUC-CN
                //CP936
                //HZ
                //EUC-TW
                //BIG-5
                //EUC-KR
                //UHC
                //ISO-2022-KR
                //Windows-1251
                //CP866
                //KOI8-R
                //KOI8-U
                //ArmSCII-8
                //CP850
            };


            public override Encoding GetEncoding(int codepage)
            {
                return null;
            }

            public static bool IsUtfEncoding(Encoding enc)
            {
                return enc is UnicodeEncoding || enc == Encoding.UTF8 || enc == Encoding.UTF32;
            }

            public override Encoding GetEncoding(string name)
            {
                var encname = new EncodingName(name);

                // encoding names used in PHP
                if (s_encmap.TryGetValue(encname, out var getter))
                {
                    return getter?.Invoke();
                }

                // cp{CodePage}
                // Codepage - {CodePage}
                if (encname.IsCodePageEncoding(out var codepage))
                {
                    return Encoding.GetEncoding(codepage);
                }

                //
                return null;
            }

            ///// <summary>
            ///// 8-bit PHP encoding.
            ///// Converts bytes to string by simply extending each byte to two-byte char.
            ///// .NET string is converted to bytes using UTF-8.
            ///// </summary>
            //sealed class EightBitEncoding : Encoding
            //{
            //    public static readonly Encoding Instance = new EightBitEncoding();

            //    private EightBitEncoding() { }

            //    public override int GetCharCount(byte[] bytes, int index, int count) => count;

            //    public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
            //    {
            //        for (int i = 0; i < byteCount; i++)
            //        {
            //            chars[charIndex + i] = (char)bytes[byteIndex + i];
            //        }

            //        return byteCount;
            //    }

            //    public override int GetMaxCharCount(int byteCount) => byteCount;

            //    public override int GetByteCount(char[] chars, int index, int count) => UTF8.GetByteCount(chars, index, count);

            //    public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
            //        => UTF8.GetBytes(chars, charIndex, charCount, bytes, byteIndex);

            //    public override int GetMaxByteCount(int charCount) => UTF8.GetMaxByteCount(charCount);
            //}
        }

        /// <summary>
        /// Get encoding based on the PHP name. Can return <c>null</c> if such encoding is not defined.
        /// </summary>
        /// <param name="encodingName"></param>
        /// <returns></returns>
        internal static Encoding GetEncoding(string encodingName)
        {
            Encoding encoding = null;
            if (encodingName != null)
            {
                try
                {
                    encoding = Encoding.GetEncoding(encodingName);
                }
                catch (Exception)
                { }
            }
            return encoding;
        }

        static string ToString(Context ctx, PhpValue value, string forceencoding = null)
        {
            switch (value.TypeCode)
            {
                case PhpTypeCode.String: return value.String;
                case PhpTypeCode.MutableString: return ToString(ctx, value.MutableString, forceencoding);
                case PhpTypeCode.Alias: return ToString(ctx, value.Alias.Value, forceencoding);
                default: return value.ToStringOrThrow(ctx);
            }
        }

        static string ToString(Context ctx, PhpString value, string forceencoding = null)
        {
            return value.ContainsBinaryData
                ? value.ToString(GetEncoding(forceencoding) ?? GetInternalEncoding(ctx))
                : value.ToString(Encoding.UTF8);  // no bytes have to be decoded anyway
        }

        static byte[] ToBytes(Context ctx, PhpString value, string forceencoding = null)
        {
            return value.ToBytes(GetEncoding(forceencoding) ?? GetInternalEncoding(ctx));
        }

        #endregion

        #region mb_internal_encoding, mb_preferred_mime_name

        /// <summary>
        /// Get encoding used by default in the extension.
        /// </summary>
        public static string mb_internal_encoding(Context ctx)
        {
            return GetInternalEncoding(ctx).WebName;
        }

        /// <summary>
        /// Set the encoding used by the extension.
        /// </summary>
        /// <param name="ctx">Runtime context.</param>
        /// <param name="encodingName"></param>
        /// <returns>True is encoding was set, otherwise false.</returns>
        public static bool mb_internal_encoding(Context ctx, string encodingName)
        {
            Encoding enc = GetEncoding(encodingName);

            if (enc != null)
            {
                ctx.Configuration.Get<MbConfig>().InternalEncoding = enc;

                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Get a MIME charset string for a specific encoding.
        /// </summary>
        /// <param name="encoding_name">The encoding being checked. Its BodyName or PHP/Phalanger name.</param>
        /// <returns>The MIME charset string for given character encoding.</returns>
        public static string mb_preferred_mime_name(string encoding_name)
        {
            var enc = GetEncoding(encoding_name);

            // try PHP internal encodings (by PHP name) and .NET encodings name
            if (enc == null)
            {
                PhpException.ArgumentValueNotSupported(nameof(encoding_name), encoding_name);
                return null;
            }

            return enc.BodyName;// it seems to return right MIME
        }

        #endregion

        #region mb_list_encodings

        /// <summary>
        /// Returns an array of all supported encodings.
        /// </summary>
        public static PhpArray mb_list_encodings()
        {
            return new PhpArray(64)
            {
                "auto",
                "wchar",
                //byte2be
                //byte2le
                //byte4be
                //byte4le
                //BASE64
                //UUENCODE
                //HTML-ENTITIES
                //Quoted-Printable
                //7bit
                //8bit,
                //UCS-4
                //UCS-4BE
                //UCS-4LE
                //UCS-2
                //UCS-2BE
                //UCS-2LE
                "UTF-16",
                "UTF-16LE",
                "UTF-16BE",
                "UTF-32BE",
                //UTF-32LE
                "UTF-32",
                "UTF-8",
                "UTF-7",
                //UTF7-IMAP
                "ASCII",
                //"CP51932",
                //"CP936",
                //"CP866",
                //"CP850",
                //EUC-JP
                //SJIS
                //eucJP-win
                //SJIS-win
                //JIS
                //ISO-2022-JP
                //ISO-2022-JP-MS
                //"Windows-1252",
                //"Windows-1254",
                //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
                //EUC-CN
                //HZ
                //EUC-TW
                //BIG-5
                //EUC-KR
                //UHC
                //ISO-2022-KR
                //"Windows-1251",
                //KOI8-R
                //KOI8-U
                //ArmSCII-8
            };
        }

        #endregion

        #region mb_substr, mb_strcut

        [return: CastToFalse]
        public static string mb_substr(Context ctx, PhpString str, int start, int? length = default, string encoding = null)
            => SubString(ctx, str, start, length.HasValue ? length.Value : int.MaxValue, encoding);

        /// <summary>
        ///
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="str"></param>
        /// <param name="start"></param>
        /// <param name="length"></param>
        /// <param name="encoding"></param>
        /// <returns></returns>
        /// <remarks>in PHP it behaves differently, but in .NET it is an alias for mb_substr</remarks>
        [return: CastToFalse]
        public static string mb_strcut(Context ctx, PhpString str, int start, int? length = default, string encoding = null)
            => SubString(ctx, str, start, length.HasValue ? length.Value : -1, encoding);

        static string SubString(Context ctx, PhpString str, int start, int length, string encoding)
        {
            var ustr = ToString(ctx, str, encoding);

            if (PhpMath.AbsolutizeRange(ref start, ref length, ustr.Length))
            {
                return ustr.Substring(start, length);
            }
            else
            {
                return null; // FALSE
            }
        }

        #endregion

        #region mb_substr_count

        /// <summary>
        /// Alias to <see cref="Strings.substr_count(string, string, int, int)"/>.
        /// </summary>
        public static int mb_substr_count(string haystack, string needle, string encoding = null)
        {
            return Strings.substr_count(haystack, needle);
        }

        #endregion

        #region mb_str_split

        /// <summary>
        /// Performs string splitting to an array of chunks of size <c>1</c>.
        /// </summary>
        public static PhpArray mb_str_split(string @string) => Strings.Split(@string);

        /// <summary>
        /// Performs string splitting to an array of defined size chunks.
        /// </summary>
        public static PhpArray mb_str_split(Context ctx, PhpString @string, int split_length = 1, string encoding = null)
        {
            return Strings.str_split(ctx, @string, split_length);

            // NOTE: then we should encode it back to byte[] using:
            // Encoding encode = GetEncoding(encoding) ?? ctx.Configuration.Get<MbConfig>().InternalEncoding
        }

        #endregion

        #region mb_strtoupper, mb_strtolower

        public static string mb_strtoupper(Context ctx, PhpValue str, string encoding = null)
            => ToString(ctx, str, encoding).ToUpperInvariant();

        public static string mb_strtolower(Context ctx, PhpValue str, string encoding = null)
            => ToString(ctx, str, encoding).ToLowerInvariant();

        #endregion

        #region mb_strlen, mb_strwidth

        /// <summary>
        /// Counts characters in a Unicode string or multi-byte string in PhpBytes.
        /// </summary>
        public static int mb_strlen(Context ctx, PhpString str, string encoding = null)
        {
            if (encoding != null && ((EncodingName)encoding).Is8bit())
            {
                // gets byte count in str
                return str.GetByteCount(ctx.StringEncoding);
            }

            // encode to unicode string using provided encoding,
            // return characters count<commit_msg>mb_substr: respects 8bit encoding<commit_after>﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Pchp.Core;
using Pchp.Core.Reflection;
using Pchp.Core.Text;
using static Pchp.Library.StandardPhpOptions;

namespace Pchp.Library
{
    [PhpExtension(ExtensionName, Registrator = typeof(Registrator))]
    public static class MultiByteString
    {
        const string ExtensionName = "mbstring";

        #region MbConfig, Options

        sealed class MbConfig : IPhpConfiguration
        {
            public Encoding InternalEncoding { get; set; }

            public Encoding RegexEncoding { get; set; }

            public Encoding HttpOutputEncoding { get; set; }

            public IPhpConfiguration Copy() => (MbConfig)this.MemberwiseClone();

            public string ExtensionName => MultiByteString.ExtensionName;

            public Encoding[] DetectOrder { get; set; } = new[] { Encoding.ASCII, Encoding.UTF8 };

            /// <summary>
            /// Gets or sets a value of a legacy configuration option.
            /// </summary>
            private static PhpValue GetSet(Context ctx, IPhpConfigurationService config, string option, PhpValue value, IniAction action)
            {
                var local = config.Get<MbConfig>();
                if (local == null)
                {
                    return PhpValue.False;
                }

                switch (option)
                {
                    //mbstring.language   "neutral"   PHP_INI_ALL Available since PHP 4.3.0.PHP_INI_PERDIR in PHP <= 5.2.6
                    //mbstring.detect_order NULL    PHP_INI_ALL Available since PHP 4.0.6.
                    //mbstring.http_input "pass"  PHP_INI_ALL Available since PHP 4.0.6.Deprecated in PHP 5.6.0.
                    //mbstring.http_output    "pass"  PHP_INI_ALL Available since PHP 4.0.6.Deprecated in PHP 5.6.0.
                    //mbstring.script_encoding    NULL PHP_INI_ALL Available since PHP 4.3.0.Removed in PHP 5.4.0.Use zend.script_encoding instead.
                    //mbstring.substitute_character NULL    PHP_INI_ALL Available since PHP 4.0.6.

                    case "mbstring.func_overload":
                        // "0" PHP_INI_SYSTEM PHP_INI_PERDIR from PHP 4.3 to 5.2.6, otherwise PHP_INI_SYSTEM. Available since PHP 4.2.0.Deprecated in PHP 7.2.0.
                        return (PhpValue)0;

                    //mbstring.encoding_translation   "0" PHP_INI_PERDIR Available since PHP 4.3.0.
                    //mbstring.strict_detection   "0" PHP_INI_ALL Available since PHP 5.1.2.
                    //mbstring.internal_encoding  NULL PHP_INI_ALL Available since PHP 4.0.6.Deprecated in PHP 5.6.0.

                    default:
                        break;
                }

                Debug.Fail("Option '" + option + "' is not currently supported.");
                return PhpValue.False;
            }

            /// <summary>
            /// Registers legacy ini-options.
            /// </summary>
            internal static void RegisterLegacyOptions()
            {
                var d = new GetSetDelegate(GetSet);

                //Register("mbstring.internal_encoding", IniFlags.Supported | IniFlags.Local, d, s);
                Register("mbstring.func_overload", IniFlags.Supported | IniFlags.Local, d, MultiByteString.ExtensionName);
            }
        }

        static MbConfig GetConfig(Context ctx) => ctx.Configuration.Get<MbConfig>();

        internal class Registrator
        {
            public Registrator()
            {
                Context.RegisterConfiguration(new MbConfig());
                MbConfig.RegisterLegacyOptions();
                Encoding.RegisterProvider(new PhpEncodingProvider());
            }
        }

        #endregion

        #region Constants

        [Flags, PhpHidden]
        public enum OverloadConstants
        {
            MB_OVERLOAD_MAIL = 1,

            MB_OVERLOAD_STRING = 2,

            MB_OVERLOAD_REGEX = 4,
        }

        public const int MB_OVERLOAD_MAIL = (int)OverloadConstants.MB_OVERLOAD_MAIL;
        public const int MB_OVERLOAD_STRING = (int)OverloadConstants.MB_OVERLOAD_STRING;
        public const int MB_OVERLOAD_REGEX = (int)OverloadConstants.MB_OVERLOAD_REGEX;

        [Flags, PhpHidden]
        public enum CaseConstants
        {
            MB_CASE_UPPER = 0,

            MB_CASE_LOWER = 1,

            MB_CASE_TITLE = 2,

            MB_CASE_FOLD = 3,

            MB_CASE_UPPER_SIMPLE = 4,

            MB_CASE_LOWER_SIMPLE = 5,

            MB_CASE_TITLE_SIMPLE = 6,

            MB_CASE_FOLD_SIMPLE = 7,
        }

        public const int MB_CASE_UPPER = (int)CaseConstants.MB_CASE_UPPER;
        public const int MB_CASE_LOWER = (int)CaseConstants.MB_CASE_LOWER;
        public const int MB_CASE_TITLE = (int)CaseConstants.MB_CASE_TITLE;
        public const int MB_CASE_FOLD = (int)CaseConstants.MB_CASE_FOLD;
        public const int MB_CASE_UPPER_SIMPLE = (int)CaseConstants.MB_CASE_UPPER_SIMPLE;
        public const int MB_CASE_LOWER_SIMPLE = (int)CaseConstants.MB_CASE_LOWER_SIMPLE;
        public const int MB_CASE_TITLE_SIMPLE = (int)CaseConstants.MB_CASE_TITLE_SIMPLE;
        public const int MB_CASE_FOLD_SIMPLE = (int)CaseConstants.MB_CASE_FOLD_SIMPLE;

        #endregion

        #region Encodings

        /// <summary>
        /// Encoding name.
        /// Provides case insensitive comparison.
        /// </summary>
        readonly struct EncodingName : IEquatable<EncodingName>, IEquatable<string>
        {
            public string Name { get; }

            static readonly HashSet<string> s_unicodenames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "utf8", "utf-8", "utf16", "utf-16", "utf-16le", "utf-16be", "unicode", "utf",
            };

            public EncodingName(string name)
            {
                Name = name;
            }

            public static implicit operator EncodingName(string name) => new EncodingName(name);

            public bool IsCodePageEncoding(out int codepage) => IsCodePageEncoding(Name, out codepage);

            public static bool IsCodePageEncoding(string name, out int codepage)
            {
                if (name != null)
                {
                    // cp{CodePage}
                    if (name.StartsWith("cp", StringComparison.OrdinalIgnoreCase) &&
                        name.Length > 2 &&
                        int.TryParse(name.Substring(2), out codepage)) // TODO: netstandard2.1 ReadOnlySpan
                    {
                        return true;
                    }

                    // Codepage - {CodePage}
                    const string CodepagePrefix = "Codepage - ";
                    if (name.StartsWith(CodepagePrefix, StringComparison.OrdinalIgnoreCase) &&
                        name.Length > CodepagePrefix.Length &&
                        int.TryParse(name.Substring(CodepagePrefix.Length), out codepage))  // TODO: netstandard2.1 ReadOnlySpan
                    {
                        return true;
                    }
                }

                //
                codepage = 0;
                return false;
            }

            public bool Is8bit() => Equals("8bit");

            public bool IsUtfEncoding() => IsUtfEncoding(Name);

            public static bool IsUtfEncoding(string name) => name != null && s_unicodenames.Contains(name);

            static bool Equals(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);

            public bool Equals(EncodingName other) => Equals(Name, other.Name);

            public bool Equals(string other) => Equals(Name, other);

            public override bool Equals(object obj)
                => obj is EncodingName encname
                    ? Equals(encname)
                    : obj is string str && Equals(new EncodingName(str));

            public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name ?? "");

        }

        sealed class PhpEncodingProvider : EncodingProvider
        {
            static readonly Dictionary<EncodingName, Func<Encoding>> s_encmap = new Dictionary<EncodingName, Func<Encoding>>()
            {
                //enc["pass"] = Encoding.Default; // TODO: "pass" encoding
                { "auto", () => Encoding.UTF8 },
                { "wchar", () => Encoding.Unicode },
                //byte2be
                //byte2le
                //byte4be
                //byte4le
                //BASE64
                //UUENCODE
                //HTML-ENTITIES
                //Quoted-Printable
                //7bit
                //8bit // EightBitEncoding.Instance
                //UCS-4
                //UCS-4BE
                //UCS-4LE
                //UCS-2
                //UCS-2BE
                //UCS-2LE
                //UTF-32
                //UTF-32BE
                //UTF-32LE
                //UTF-16
                //UTF-16BE
                { "UTF-16LE", () => Encoding.Unicode }, // alias UTF-16
                { "UTF-8", () => Encoding.UTF8 },       // alias UTF8
                //UTF-7
                //UTF7-IMAP
                { "ASCII", () => Encoding.ASCII },      // alias us-ascii
                //EUC-JP
                //SJIS
                //eucJP-win
                //SJIS-win
                //CP51932
                //JIS
                //ISO-2022-JP
                //ISO-2022-JP-MS
                //Windows-1252
                //Windows-1254
                //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
                //EUC-CN
                //CP936
                //HZ
                //EUC-TW
                //BIG-5
                //EUC-KR
                //UHC
                //ISO-2022-KR
                //Windows-1251
                //CP866
                //KOI8-R
                //KOI8-U
                //ArmSCII-8
                //CP850
            };


            public override Encoding GetEncoding(int codepage)
            {
                return null;
            }

            public static bool IsUtfEncoding(Encoding enc)
            {
                return enc is UnicodeEncoding || enc == Encoding.UTF8 || enc == Encoding.UTF32;
            }

            public override Encoding GetEncoding(string name)
            {
                var encname = new EncodingName(name);

                // encoding names used in PHP
                if (s_encmap.TryGetValue(encname, out var getter))
                {
                    return getter?.Invoke();
                }

                // cp{CodePage}
                // Codepage - {CodePage}
                if (encname.IsCodePageEncoding(out var codepage))
                {
                    return Encoding.GetEncoding(codepage);
                }

                //
                return null;
            }

            ///// <summary>
            ///// 8-bit PHP encoding.
            ///// Converts bytes to string by simply extending each byte to two-byte char.
            ///// .NET string is converted to bytes using UTF-8.
            ///// </summary>
            //sealed class EightBitEncoding : Encoding
            //{
            //    public static readonly Encoding Instance = new EightBitEncoding();

            //    private EightBitEncoding() { }

            //    public override int GetCharCount(byte[] bytes, int index, int count) => count;

            //    public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
            //    {
            //        for (int i = 0; i < byteCount; i++)
            //        {
            //            chars[charIndex + i] = (char)bytes[byteIndex + i];
            //        }

            //        return byteCount;
            //    }

            //    public override int GetMaxCharCount(int byteCount) => byteCount;

            //    public override int GetByteCount(char[] chars, int index, int count) => UTF8.GetByteCount(chars, index, count);

            //    public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
            //        => UTF8.GetBytes(chars, charIndex, charCount, bytes, byteIndex);

            //    public override int GetMaxByteCount(int charCount) => UTF8.GetMaxByteCount(charCount);
            //}
        }

        /// <summary>
        /// Get encoding based on the PHP name. Can return <c>null</c> if such encoding is not defined.
        /// </summary>
        /// <param name="encodingName"></param>
        /// <returns></returns>
        internal static Encoding GetEncoding(string encodingName)
        {
            Encoding encoding = null;
            if (encodingName != null)
            {
                try
                {
                    encoding = Encoding.GetEncoding(encodingName);
                }
                catch (Exception)
                { }
            }
            return encoding;
        }

        static string ToString(Context ctx, PhpValue value, string forceencoding = null)
        {
            switch (value.TypeCode)
            {
                case PhpTypeCode.String: return value.String;
                case PhpTypeCode.MutableString: return ToString(ctx, value.MutableString, forceencoding);
                case PhpTypeCode.Alias: return ToString(ctx, value.Alias.Value, forceencoding);
                default: return value.ToStringOrThrow(ctx);
            }
        }

        static string ToString(Context ctx, PhpString value, string forceencoding = null)
        {
            return value.ContainsBinaryData
                ? value.ToString(GetEncoding(forceencoding) ?? GetInternalEncoding(ctx))
                : value.ToString(Encoding.UTF8);  // no bytes have to be decoded anyway
        }

        static byte[] ToBytes(Context ctx, PhpString value, string forceencoding = null)
        {
            return value.ToBytes(GetEncoding(forceencoding) ?? GetInternalEncoding(ctx));
        }

        #endregion

        #region mb_internal_encoding, mb_preferred_mime_name

        /// <summary>
        /// Get encoding used by default in the extension.
        /// </summary>
        public static string mb_internal_encoding(Context ctx)
        {
            return GetInternalEncoding(ctx).WebName;
        }

        /// <summary>
        /// Set the encoding used by the extension.
        /// </summary>
        /// <param name="ctx">Runtime context.</param>
        /// <param name="encodingName"></param>
        /// <returns>True is encoding was set, otherwise false.</returns>
        public static bool mb_internal_encoding(Context ctx, string encodingName)
        {
            Encoding enc = GetEncoding(encodingName);

            if (enc != null)
            {
                ctx.Configuration.Get<MbConfig>().InternalEncoding = enc;

                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Get a MIME charset string for a specific encoding.
        /// </summary>
        /// <param name="encoding_name">The encoding being checked. Its BodyName or PHP/Phalanger name.</param>
        /// <returns>The MIME charset string for given character encoding.</returns>
        public static string mb_preferred_mime_name(string encoding_name)
        {
            var enc = GetEncoding(encoding_name);

            // try PHP internal encodings (by PHP name) and .NET encodings name
            if (enc == null)
            {
                PhpException.ArgumentValueNotSupported(nameof(encoding_name), encoding_name);
                return null;
            }

            return enc.BodyName;// it seems to return right MIME
        }

        #endregion

        #region mb_list_encodings

        /// <summary>
        /// Returns an array of all supported encodings.
        /// </summary>
        public static PhpArray mb_list_encodings()
        {
            return new PhpArray(64)
            {
                "auto",
                "wchar",
                //byte2be
                //byte2le
                //byte4be
                //byte4le
                //BASE64
                //UUENCODE
                //HTML-ENTITIES
                //Quoted-Printable
                //7bit
                //8bit,
                //UCS-4
                //UCS-4BE
                //UCS-4LE
                //UCS-2
                //UCS-2BE
                //UCS-2LE
                "UTF-16",
                "UTF-16LE",
                "UTF-16BE",
                "UTF-32BE",
                //UTF-32LE
                "UTF-32",
                "UTF-8",
                "UTF-7",
                //UTF7-IMAP
                "ASCII",
                //"CP51932",
                //"CP936",
                //"CP866",
                //"CP850",
                //EUC-JP
                //SJIS
                //eucJP-win
                //SJIS-win
                //JIS
                //ISO-2022-JP
                //ISO-2022-JP-MS
                //"Windows-1252",
                //"Windows-1254",
                //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
                //EUC-CN
                //HZ
                //EUC-TW
                //BIG-5
                //EUC-KR
                //UHC
                //ISO-2022-KR
                //"Windows-1251",
                //KOI8-R
                //KOI8-U
                //ArmSCII-8
            };
        }

        #endregion

        #region mb_substr, mb_strcut

        /// <summary>
        /// Gets a substring, start and length refers to actual encoded characters.
        /// </summary>
        [return: CastToFalse]
        public static PhpString mb_substr(Context ctx, PhpString str, int start, int? length = default, string encoding = null)
        {
            var lengthValue = length.HasValue ? length.GetValueOrDefault() : int.MaxValue;

            if (((EncodingName)encoding).Is8bit())
            {
                return SubString(str.ToBytes(ctx), start, lengthValue);
            }

            return SubString(ctx, str, start, lengthValue, encoding);
        }

        /// <summary>
        /// Gets a substring, unlike <see cref="mb_substr"/>, start and length refers to bytes in the input string.
        /// If the poition points inside a multi-byte character, it gets adjusted to the beginning of that character.
        /// </summary>
        [return: CastToFalse]
        public static string mb_strcut(Context ctx, PhpString str, int start, int? length = default, string encoding = null)
        {
            if (((EncodingName)encoding).Is8bit())
            {
                // TODO: return SubString(str.ToBytes(ctx), start, lengthValue);
            }

            // TODO: handle {start} and {length} properly - as byte-offsets, not char offsets

            return SubString(ctx, str, start, length.HasValue ? length.GetValueOrDefault() : -1, encoding);
        }

        static PhpString SubString(byte[] str, int start, int length)
        {
            if (PhpMath.AbsolutizeRange(ref start, ref length, str.Length))
            {
                if (start == 0 && length == str.Length)
                {
                    return new PhpString(str);
                }

                return new PhpString(str.Slice(start, length));
            }
            else
            {
                return default; // FALSE
            }
        }

        static string SubString(Context ctx, PhpString str, int start, int length, string encoding)
        {
            var ustr = ToString(ctx, str, encoding);

            if (PhpMath.AbsolutizeRange(ref start, ref length, ustr.Length))
            {
                return ustr.Substring(start, length);
            }
            else
            {
                return null; // FALSE
            }
        }

        #endregion

        #region mb_substr_count

        /// <summary>
        /// Alias to <see cref="Strings.substr_count(string, string, int, int)"/>.
        /// </summary>
        public static int mb_substr_count(string haystack, string needle, string encoding = null)
        {
            return Strings.substr_count(haystack, needle);
        }

        #endregion

        #region mb_str_split

        /// <summary>
        /// Performs string splitting to an array of chunks of size <c>1</c>.
        /// </summary>
        public static PhpArray mb_str_split(string @string) => Strings.Split(@string);

        /// <summary>
        /// Performs string splitting to an array of defined size chunks.
        /// </summary>
        public static PhpArray mb_str_split(Context ctx, PhpString @string, int split_length = 1, string encoding = null)
        {
            return Strings.str_split(ctx, @string, split_length);

            // NOTE: then we should encode it back to byte[] using:
            // Encoding encode = GetEncoding(encoding) ?? ctx.Configuration.Get<MbConfig>().InternalEncoding
        }

        #endregion

        #region mb_strtoupper, mb_strtolower

        public static string mb_strtoupper(Context ctx, PhpValue str, string encoding = null)
            => ToString(ctx, str, encoding).ToUpperInvariant();

        public static string mb_strtolower(Context ctx, PhpValue str, string encoding = null)
            => ToString(ctx, str, encoding).ToLowerInvariant();

        #endregion

        #region mb_strlen, mb_strwidth

        /// <summary>
        /// Counts characters in a Unicode string or multi-byte string in PhpBytes.
        /// </summary>
        public static int mb_strlen(Context ctx, PhpString str, string encoding = null)
        {
            if (((EncodingName)encoding).Is8bit())
            {
                // gets byte count in str
                return str.GetByteCount(ctx.StringEncoding);
            }

            // encode to unicode string using provided encoding,
            // return characters count
{'fraction_non_alphanumeric': 0.09862369922792884, 'fraction_numerical': 0.018037372720152177, 'mean_word_length': 1.3179790434692396, 'pattern_counts': {'":': 2, '<': 142, '<?xml version=': 0, '>': 196, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7235390', 'n_tokens_mistral': 12622, 'n_tokens_neox': 11481, 'n_words': 2800}