using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.CodeAnalysis;
using System.Data;

namespace Wayne.Lib
{
    /// <summary>
    /// Static support class for string manipulation.
    /// </summary>
    public static class Strings
    {
        /// <summary>
        /// Tries to parse an integer just like the full framework's int.TryParse()
        /// </summary>
        /// <param name="possibleInt">String containing integer</param>
        /// <param name="i">Contains the integer values if the operation is successful</param>
        /// <returns>True if the parse was successful, false otherwise</returns>
        public static bool TryParseInt(string possibleInt, out int i)
        {
            if (!string.IsNullOrEmpty(possibleInt))
            {
                try
                {
                    if (possibleInt.All(Char.IsNumber))
                    {
                        i = int.Parse(possibleInt);
                        return true;
                    }
                }
                catch (FormatException)
                { }

            }
            i = 0;
            return false;
        }

        /// <summary>
        /// Tries to pares a byte just like the full framework's byte.TryParse()
        /// </summary>
        /// <param name="possibleInt">String containing the byte value</param>
        /// <param name="b">Out parameter for the result.</param>
        /// <returns>True if parsed successfully.</returns>
        [System.Diagnostics.DebuggerStepThrough]
        public static bool TryParseByte(string possibleInt, out byte b)
        {
            if (!string.IsNullOrEmpty(possibleInt))
            {
                try
                {
                    b = byte.Parse(possibleInt);
                    return true;
                }
                catch (FormatException)
                { }
                catch (OverflowException)
                { }

            }
            b = 0;
            return false;
        }

        /// <summary>
        /// Tries to parse an integer and returning null if it is not possible.
        /// </summary>
        /// <param name="possibleInt">String containing integer</param>
        /// <returns>The parsed integer or null if System.Int32.Parse() threw an FormatException.</returns>
        public static int? ParseInt(string possibleInt)
        {
#if WindowsCE
            if (!string.IsNullOrEmpty(possibleInt))
            {
                try
                {
                    int i = int.Parse(possibleInt);
                    return i;
                }
                catch (FormatException)
                { }

            }
#else

            int result;
            if (int.TryParse(possibleInt, out result))
                return result;
#endif
            return null;
        }

        #region Connection string encoding and decoding.

        /// <summary>
        /// Parses a standard Wayne connection string on the form "ParamName=Value,ParamName2=Value2" and 
        /// returns it as a Dictionary&lt;string,string&gt;, with key/value corresponding to the original string.
        /// </summary>
        /// <param name="connectionString">A Wayne connection string.</param>
        /// <returns>A dictionary with the parsed param/values.</returns>
        public static Dictionary<string, string> ParseConnectionString(string connectionString)
        {
            Dictionary<string, string> connectionStringParamDict = new Dictionary<string, string>();
            string[] listItems = connectionString.Split(',');
            foreach (string listItem in listItems)
            {
                string trimmedListItem = listItem.Trim();
                if (trimmedListItem.Length > 0)
                {
                    int equalSignIndex = trimmedListItem.IndexOf('=');
                    if (equalSignIndex <= 0)
                        throw new ArgumentException("ParseConnectionString, badly formatted connectionString: " + connectionString + " at " + trimmedListItem);
                    string paramName = trimmedListItem.Substring(0, equalSignIndex);
                    string paramValue = trimmedListItem.Substring(equalSignIndex + 1, trimmedListItem.Length - equalSignIndex - 1);
                    connectionStringParamDict[paramName] = paramValue;
                }
            }
            return connectionStringParamDict;
        }

        /// <summary>
        /// Creates a connection string from a string,string dictionary.
        /// </summary>
        /// <param name="connectionStringDictionary"></param>
        /// <returns></returns>
        public static string CreateConnectionString(Dictionary<string, string> connectionStringDictionary)
        {
            if (connectionStringDictionary == null)
                throw new ArgumentNullException("connectionStringDictionary");

            StringBuilder stringBuilder = new StringBuilder();
            foreach (KeyValuePair<string, string> pair in connectionStringDictionary)
            {
                stringBuilder.Append(pair.Key);
                stringBuilder.Append("=");
                stringBuilder.Append(pair.Value);
                stringBuilder.Append(",");
            }

            //Remove the last comma 
            if (stringBuilder.Length > 0)
                stringBuilder.Remove(stringBuilder.Length - 1, 1);
            return stringBuilder.ToString();
        }

        #endregion

        #region ASCII numeric string to BCD in a string conversions
        /// <summary>
        /// 8-bit encoding - uses codepage 1252 right now
        /// </summary>
        public static System.Text.Encoding EightBitEncoding = System.Text.Encoding.GetEncoding(1252);

        /// <summary>
        /// turns a string containing numeric ASCII to a string containing BCD
        /// </summary>
        /// <param name="ascii">input string, containing (e.g.) "1324"</param>
        /// <param name="length">desired number of digits, rounded up to nearest even number. output will contain length/2 bytes</param>
        /// <returns>output string, containing BCD form</returns>
        public static string NumericAsciiToBCD(string ascii, int length)
        {
            length = (length % 2) == 0 ? length : length + 1;
            string f = (ascii.Length > length) ? ascii.Substring(0, length) : ascii.PadLeft(length, '0');
            byte[] fb = EightBitEncoding.GetBytes(f);
            byte[] ob = new byte[fb.Length / 2];
            byte bip = 0;
            for (int i = 0; i < fb.Length; i++)
            {
                if (fb[i] < 0x30 || fb[i] > 0x39)
                {
                    throw new ArgumentException("Input contains non-numeric ASCII character '" + fb[i] + "' at position " + i);
                }
                if ((i % 2) == 0)
                {
                    bip = fb[i];
                    bip = (byte)(bip - 0x30);
                    bip = (byte)(bip << 4);
                }
                else
                {
                    bip = (byte)(bip | ((byte)(fb[i] - 0x30)));
                    ob[i / 2] = bip;
                }
            }
            return EightBitEncoding.GetString(ob, 0, ob.Length);
        }

        private static char[] BCDMap = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

        /// <summary>
        /// Converts BCD coded data into ASCII.
        /// </summary>
        /// <param name="ascii"></param>
        /// <returns></returns>
        public static string BCDToNumericAscii(string ascii)
        {
            int length = ascii.Length;
            byte[] fb = EightBitEncoding.GetBytes(ascii);
            StringBuilder sb = new StringBuilder();
            byte bip0, bip1;
            for (int i = 0; i < fb.Length; i++)
            {
                bip0 = (byte)((fb[i] >> 4) & 0x0f);
                bip1 = (byte)(fb[i] & 0xf0);
                if (bip0 > 9 || bip1 > 9)
                {
                    throw new ArgumentException("Input contains non-BCD character '" + fb[i] + "' at position " + i);
                }
                sb.Append(BCDMap[bip0] - 0x30);
                sb.Append(BCDMap[bip1] - 0x30);
            }
            return sb.ToString();
        }


        /// <summary>
        /// Shows a hex dump of a string taken as bytes
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static string StringAsHexString(string s)
        {
            byte[] bytes = EightBitEncoding.GetBytes(s);
            return ByteArrayToString(bytes, StringsByteArrayFormat.Hex).Replace(',', ' ');
        }

        #endregion

        #region Byte array conversions

        /// <summary>
        /// Converts a byte array to a hexadecimal representation in the form of a string.
        /// <note>Use StringToByteArray to get the array back.</note>
        /// </summary>
        /// <param name="bytes">Byte array.</param>
        /// <param name="offset">The index of the first byte to read.</param>
        /// <param name="length">The number of bytes to read.</param>
        /// <param name="stringsByteArrayFormat">The format of the output</param>
        /// <seealso cref="StringToByteArray"/>
        /// <returns></returns>
        /// <exception cref="ArgumentException">Thrown when the conversion is not possible.</exception>
        public static string ByteArrayToString(byte[] bytes, int offset, int length, StringsByteArrayFormat stringsByteArrayFormat)
        {
            if ((bytes == null) || (bytes.Length == 0) || (length == 0))
                return string.Empty;

            if (stringsByteArrayFormat == StringsByteArrayFormat.ANSI)
            {
                char[] charArray = System.Text.Encoding.Default.GetChars(bytes, offset, length);
                if (length != charArray.Length)
                    throw new ArgumentException("Unable to convert the byte array to an ANSI string.");
                StringBuilder result = new StringBuilder(charArray.Length);
                for (int i = 0; i < charArray.Length; i++)
                {
                    if (charArray[i] == '<')
                        result.Append("<<");
                    else if ((charArray[i] == bytes[i + offset]) && (charArray[i] >= ' '))
                        result.Append(charArray[i]);
                    else
                    {
                        result.Append("<");
                        result.Append(bytes[i + offset].ToString("X2"));
                        result.Append(">");
                    }
                }
                return result.ToString();
            }
            else if (stringsByteArrayFormat == StringsByteArrayFormat.CompactANSI)
            {
                char[] charArray = System.Text.Encoding.Default.GetChars(bytes, offset, length);
                if (length != charArray.Length)
                    throw new ArgumentException("Unable to convert the byte array to an ANSI string.");
                for (int i = 0; i < charArray.Length; i++)
                    if ((charArray[i] != bytes[i + offset]) || (charArray[i] < ' '))
                        charArray[i] = '?';
                return new string(charArray);
            }
            else if (stringsByteArrayFormat == StringsByteArrayFormat.String)
            {
                return bytes == null ? null : EightBitEncoding.GetString(bytes, 0, bytes.Length);
            }
            else
            {
                bool commaSeparated;
                string format;
                switch (stringsByteArrayFormat)
                {
                    case StringsByteArrayFormat.Bytes:
                        commaSeparated = true;
                        format = "G";
                        break;
                    case StringsByteArrayFormat.Hex:
                        commaSeparated = true;
                        format = "X2";
                        break;
                    case StringsByteArrayFormat.CompactHex:
                        commaSeparated = false;
                        format = "X2";
                        break;
                    default:
                        throw new ArgumentException("Unsupported StringsByteArrayFormat", "stringsByteArrayFormat");
                }
                StringBuilder result = new StringBuilder(length);
                for (int arrayIndex = 0; arrayIndex < length; arrayIndex++)
                {
                    if (commaSeparated && (arrayIndex > 0))
                        result.Append(",");
                    result.Append(bytes[arrayIndex + offset].ToString(format, System.Globalization.CultureInfo.InvariantCulture));
                }
                return result.ToString();
            }
        }

        /// <summary>
        /// Converts a byte array to a hexadecimal representation in the form of a string.
        /// <note>Use StringToByteArray to get the array back.</note>
        /// </summary>
        /// <param name="bytes">Byte array.</param>
        /// <param name="stringsByteArrayFormat">The format of the output</param>
        /// <seealso cref="StringToByteArray"/>
        /// <returns></returns>
        /// <exception cref="ArgumentException">Thrown when the conversion is not possible.</exception>
        public static string ByteArrayToString(byte[] bytes, StringsByteArrayFormat stringsByteArrayFormat)
        {
            if (bytes == null)
                return string.Empty;
            return ByteArrayToString(bytes, 0, bytes.Length, stringsByteArrayFormat);
        }

        /// <summary>
        /// Converts a byte array string back to a byte array.
        /// </summary>
        /// <param name="bytes">Byte array string.</param>
        /// <param name="stringsByteArrayFormat"></param>
        /// <seealso cref="ByteArrayToString(byte[],Wayne.Lib.StringsByteArrayFormat)"/>
        /// <seealso cref="ByteArrayToString(byte[],int,int,Wayne.Lib.StringsByteArrayFormat)"/>
        /// <returns></returns>
        /// <exception cref="ArgumentException">Input string does not have the correct format.</exception>
        [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.ArgumentException.#ctor(System.String)")]
        public static byte[] StringToByteArray(string bytes, StringsByteArrayFormat stringsByteArrayFormat)
        {
            if (string.IsNullOrEmpty(bytes))
                return new byte[0];

            switch (stringsByteArrayFormat)
            {
                case StringsByteArrayFormat.Raw:
                    {
                        byte[] results = EightBitEncoding.GetBytes(bytes);
                        return results;
                    }
                //-------------------------------------
                case StringsByteArrayFormat.Bytes:
                    {
                        string[] stringArray = bytes.Split(',');
                        byte[] result = new byte[stringArray.Length];
                        for (int i = 0; i < stringArray.Length; i++)
                            result[i] = Convert.ToByte(stringArray[i]);
                        return result;
                    }
                //-------------------------------------
                case StringsByteArrayFormat.Hex:
                    {
                        string[] stringArray = bytes.Split(',');
                        byte[] result = new byte[stringArray.Length];
                        for (int i = 0; i < stringArray.Length; i++)
                        {
                            string hexNumber = stringArray[i];
                            if (hexNumber.Length == 2)
                                result[i] = (byte)((byte)(GetHex(hexNumber[0]) << 4) + GetHex(hexNumber[1]));
                            else
                                throw new ArgumentException("The hex string had a bad hex value (2 hex digits required).");
                        }
                        return result;
                    }
                //-------------------------------------
                case StringsByteArrayFormat.CompactHex:
                    {
                        int hexStringLength = bytes.Length;
                        int arrayLength = (hexStringLength + 1) / 2;
                        byte[] result = new byte[arrayLength];

                        int hexStringIndex = 0;
                        int arrayIndex = 0;

                        if (hexStringLength % 2 != 0)
                            result[arrayIndex++] = GetHex(bytes[hexStringIndex++]);

                        for (; arrayIndex < arrayLength; arrayIndex++, hexStringIndex += 2)
                            result[arrayIndex] = (byte)((byte)(GetHex(bytes[hexStringIndex]) << 4) + GetHex(bytes[hexStringIndex + 1]));
                        return result;
                    }
                //-------------------------------------
                case StringsByteArrayFormat.ANSI:
                    {
                        // Calculate the length of the resulting byte array.
                        int byteArrayLength = bytes.Length;
                        for (int i = 0; i < bytes.Length; i++)
                        {
                            if (bytes[i] == '<')
                            {
                                // Escape character found. Is it a '<<' or '<nn>'?
                                if ((i == bytes.Length - 1) || (bytes[i + 1] == '<'))
                                {
                                    // It's a '<<' (or a badly formatted string ending in one '<').
                                    byteArrayLength--; // The byte array is one characted shorter than expected.
                                    i++; // Skip the next character.
                                }
                                else
                                {
                                    // It's a '<nn>'.
                                    byteArrayLength -= 3; // The byte array is three characted shorter than expected.
                                    i += 3; // Skip the next three characters.
                                }
                            }
                        }
                        byte[] result = new byte[byteArrayLength];
                        int byteArrayIndex = 0;
                        for (int i = 0; i < bytes.Length; i++)
                        {
                            if (bytes[i] == '<')
                            {
                                // Escape character found. Is it a '<<' or '<nn>'?
                                if ((i == bytes.Length - 1) || (bytes[i + 1] == '<'))
                                {
                                    // It's a '<<' (or a badly formatted string ending in one '<').
                                    result[byteArrayIndex] = (byte)bytes[i];
                                    i++; // Skip the next character.
                                }
                                else if (i <= bytes.Length - 4)
                                {
                                    // It's a '<nn>'.
                                    result[byteArrayIndex] = (byte)((byte)(GetHex(bytes[i + 1]) << 4) + GetHex(bytes[i + 2]));
                                    i += 3; // Skip the next three characters.
                                }
                                else
                                    throw new ArgumentException("Badly formatted ANSI string. An uncompleted '<nn>' found in the end of the string.");
                            }
                            else
                            {
                                result[byteArrayIndex] = (byte)bytes[i];
                            }
                            byteArrayIndex++;
                        }

                        return result;
                    }
                //-------------------------------------
                case StringsByteArrayFormat.CompactANSI:
                default:
                    throw new ArgumentException("Unsupported StringsByteArrayFormat", "stringsByteArrayFormat");
            }
        }

        [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.ArgumentException.#ctor(System.String)")]
        private static byte GetHex(char hex)
        {
            if ((hex >= '0') && (hex <= '9'))
                return (byte)(hex - '0');
            else if ((hex >= 'a') && (hex <= 'f'))
                return (byte)(hex - 'a' + 10);
            else if ((hex >= 'A') && (hex <= 'F'))
                return (byte)(hex - 'A' + 10);
            throw new ArgumentException("The hexString contains a non-hex character ('" + hex + "'). Allowed characters are 0-9, a-f and A-F.");
        }

        #endregion

        #region SaveToFile / LoadFromFile (==OBSOLETE==)

        /// <summary>
        /// Saves an array of strings to a file.
        /// </summary>
        /// <param name="fileName">The path and file name.</param>
        /// <param name="lines">The lines to save.</param>
        [Obsolete("This method is now obsolete. Use the Wayne.Lib.IO.FileSupport.SaveToFile() instead.", true)]
        public static void SaveToFile(string fileName, string[] lines)
        {
        }

        /// <summary>
        /// Saves the content of a StringBuilder to a file.
        /// </summary>
        /// <param name="fileName">The path and file name.</param>
        /// <param name="lines">The lines to save.</param>
        [Obsolete("This method is now obsolete. Use the Wayne.Lib.IO.FileSupport.SaveToFile() instead.", true)]
        public static void SaveToFile(string fileName, StringBuilder lines)
        {
        }

        /// <summary>
        /// Loads a text file into an array of strings.
        /// </summary>
        /// <param name="fileName">The path and file name.</param>
        [Obsolete("This method is now obsolete. Use the Wayne.Lib.IO.FileSupport.LoadToStringArray() instead.", true)]
        public static string[] LoadFromFile(string fileName)
        {
            return null;
        }

        #endregion

        #region Indenting

        private static int tabLength = 8;

        /// <summary>
        /// The length of a Tab. Used by the Indent-method. Default is 8.
        /// </summary>
        public static int TabLength
        {
            get { return tabLength; }
            set { tabLength = value; }
        }

        /// <summary>
        /// Returns a padded string (e.g. used for indenting).
        /// </summary>
        /// <param name="indentLength">The indent to be used if many lines.</param>
        /// <param name="allowTabCharacter">Is tab-characters allowed to be used? The TabLength-property tells the length of a tab-character.</param>
        /// <returns></returns>
        public static string Indent(int indentLength, bool allowTabCharacter)
        {
            if (allowTabCharacter)
            {
                StringBuilder indent = new StringBuilder();
                int tabCount = indentLength / tabLength;
                for (int i = 0; i < tabCount; i++)
                    indent.Append("\t");
                int spaceCount = indentLength - tabCount * tabLength;
                if (spaceCount > 0)
                    indent.Append("".PadRight(spaceCount, ' '));
                return indent.ToString();
            }
            else
                return "".PadRight(indentLength, ' ');
        }

        #endregion

        #region DataSet text formatting

        /// <summary>
        /// Returns an array of strings that forms a document with a formatted dataset, table after table.
        /// </summary>
        /// <param name="dataSet"></param>
        /// <returns></returns>
        public static string[] FormatDataSet(DataSet dataSet)
        {
            List<string> result = new List<string>();
            result.Add(" DataSetName : " + dataSet.DataSetName);
            result.Add(" DataSetLocale : " + dataSet.Locale.ToString());

            foreach (DataTable dataTable in dataSet.Tables)
            {
                result.Add("Table " + dataTable.TableName);
                result.Add("======================================================");

                int colCount = dataTable.Columns.Count;
                int[] colWidths = new int[colCount];

                //Take the column name for starter.
                for (int i = 0; i < dataTable.Columns.Count; i++)
                {
                    colWidths[i] = dataTable.Columns[i].ColumnName.Length;
                }

                //Run through the data, to find the column width
                foreach (DataRow dataRow in dataTable.Rows)
                {
                    for (int col = 0; col < colCount; col++)
                    {
                        string text = dataRow[col].ToString();
                        colWidths[col] = Math.Max(colWidths[col], text.Length);
                    }
                }

                StringBuilder rowBuilder = new StringBuilder();

                //Build header row.
                //Take the column name for starter.
                string[] formatStrings = new string[colCount];
                for (int i = 0; i < formatStrings.Length; i++)
                    formatStrings[i] = "{0,-" + colWidths[i].ToString() + "} ";

                for (int i = 0; i < colCount; i++)
                {
                    rowBuilder.Append(string.Format(formatStrings[i], dataTable.Columns[i].ColumnName));
                }

                string headerRow = rowBuilder.ToString();
                result.Add(headerRow);
                rowBuilder.Length = 0;

                //Put in column undescore                
                for (int i = 0; i < headerRow.Length; i++)
                {
                    if (headerRow[i] == ' ')
                        rowBuilder.Append(' ');
                    else
                        rowBuilder.Append("-");
                }
                result.Add(rowBuilder.ToString());
                rowBuilder.Length = 0;

                foreach (DataRow dataRow in dataTable.Rows)
                {
                    for (int col = 0; col < colCount; col++)
                    {
                        string colStr = string.Format(formatStrings[col], dataRow[col].ToString());
                        rowBuilder.Append(colStr);
                    }

                    result.Add(rowBuilder.ToString());
                    rowBuilder.Length = 0;
                }

                result.Add("");
            }
            return result.ToArray();

        }
        #endregion

        #region NaturalComparer

        private static readonly NaturalStringComparer naturalStringComparer = new NaturalStringComparer();

        /// <summary>
        /// An IComparer used to sort strings naturally (i.e. so that "5" comes before "10" and "x5" comes before "x10").
        /// </summary>
        public static IComparer<string> NaturalComparer { get { return naturalStringComparer; } }

        #endregion

        #region Join

        /// <summary>
        /// Concatenates a specified separator string between each element of a specified array, yielding a single concatenated string.
        /// </summary>
        /// <param name="separator">A string.</param>
        /// <param name="objects">Objects to join</param>
        /// <returns></returns>
        public static string Join<T>(string separator, IEnumerable<T> objects)
        {
            StringBuilder text = new StringBuilder();
            bool first = true;
            foreach (T data in objects)
            {
                if (first)
                    first = false;
                else
                    text.Append(separator);
                text.Append(data);
            }
            return text.ToString();
        }

        #endregion

        #region ISO8583 encoding helpers

        /// <summary>
        /// Adds decimal in string form to a buffer, preceded by optional sign. Returns total number of characters added
        /// </summary>
        /// <param name="p"></param>
        /// <param name="sb"></param>
        /// <param name="addSign"></param>
        /// <param name="totalLength"></param>
        /// <param name="decimalPlaces"></param>
        /// <param name="paddingChar"></param>
        public static int DecimalToString(decimal p, StringBuilder sb, bool addSign, int totalLength, int decimalPlaces, char paddingChar)
        {
            int rc = 0;
            if (sb != null)
            {
                if (addSign)
                {
                    sb.Append(p >= 0m ? '+' : '-');
                    rc++;
                }
                double multiplier = Math.Pow(10, decimalPlaces);
                long po = (long)Math.Abs(Decimal.Round((decimal)((double) p * multiplier), 0));
                sb.Append(po.ToString().PadLeft(totalLength, paddingChar));
                rc += totalLength;
            }
            return rc;
        }

        /// <summary>
        /// Adds decimal in string form to a buffer, preceded by optional sign. Returns total number of characters added
        /// </summary>
        /// <param name="p"></param>
        /// <param name="sb"></param>
        /// <param name="addSign"></param>
        /// <param name="totalLength"></param>
        /// <param name="decimalPlaces"></param>
        public static int DecimalToString(decimal p, StringBuilder sb, bool addSign, int totalLength, int decimalPlaces)
        {
            int rc = 0;
            if (sb != null)
            {
                if (addSign)
                {
                    sb.Append(p >= 0m ? '+' : '-');
                    rc++;
                }
                double multiplier = Math.Pow(10, decimalPlaces);
                long po = (long)Math.Abs(Decimal.Round((decimal) ((double)p * multiplier), 0));
                sb.Append(po.ToString());
                rc = sb.Length;
            }
            return rc;
        }

        #endregion

    }
}