using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;

namespace C1.Silverlight.FlexGridExcel
{
    /// <summary>
    /// Represents a MS Excel number format.
    /// </summary>
    public class XLNumberFormat
    {
        //-------------------------------------------------------------------------
        #region ** ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="XLNumberFormat"/> class.
        /// </summary>
        public XLNumberFormat()
        {
            Element = new XElement(XLWorkbook.SheetNS + XLConstants.NUMBER_FORMAT);
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="XLNumberFormat"/> class.
        /// </summary>
        /// <param name="element">The xml element to load the XLNumberFormat from.</param>
        public XLNumberFormat(XElement element)
        {
            Element = element ?? new XElement(XLWorkbook.SheetNS + XLConstants.NUMBER_FORMAT);
        }

        #endregion

        //-------------------------------------------------------------------------
        #region ** object model

        /// <summary>
        /// Gets the xml element that represents this number format.
        /// </summary>
        public XElement Element { get; private set; }
        /// <summary>
        /// Gets or sets the number format code (in Excel format).
        /// </summary>
        /// <value>The format code.</value>
        internal string FormatCode
        {
            get
            {
                if (Element.Attribute(XLConstants.FORMAT_CODE) != null)
                {
                    return Element.Attribute(XLConstants.FORMAT_CODE).Value;
                }
                return string.Empty;
            }
            set
            {
                Element.SetAttributeValue(XLConstants.FORMAT_CODE, value);
            }
        }
        /// <summary>
        /// Gets or sets the number format id.
        /// </summary>
        /// <value>The id.</value>
        public int? Id
        {
            get
            {
                if (Element.Attribute(XLConstants.NUMBER_FORMAT_ID) != null)
                {
                    return int.Parse(Element.Attribute(XLConstants.NUMBER_FORMAT_ID).Value, CultureInfo.InvariantCulture);
                }
                return null;
            }
            set
            {
                Element.SetAttributeValue(XLConstants.NUMBER_FORMAT_ID, value);
            }
        }
        /// <summary>
        /// Sets the format code for this number format (in .Net format).
        /// </summary>
        /// <param name="fmt">The number format.</param>
        /// <param name="dataType">Type of the data the format refers to.</param>
        public void SetDotNetFormat(string fmt, Type dataType)
        {
            FormatCode = FormatDotNetToXL(fmt, dataType);
        }
        /// <summary>
        /// Gets the number format (in .Net format).
        /// </summary>
        /// <returns></returns>
        public string GetDotNetFormat()
        {
            return FormatXLToDotNet(FormatCode);
        }
        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        /// 	<c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public override bool Equals(object obj)
        {
            XLNumberFormat numFmt = obj as XLNumberFormat;
            if (numFmt != null)
            {
                return this.FormatCode.Equals(numFmt.FormatCode);
            }
            return false;
        }

        #endregion

        //-------------------------------------------------------------------------
        #region ** private style-conversion utilities

        static char[] _timeChars = new char[] { 's', 'a', ':' };
        static char[] _dateChars = new char[] { 'd', 'y', '/' };
        static Regex _rxCurrency = new Regex(@"\[\$([^\-\]]+)[^\]]*\]");
        static Regex _rxCharDigits = new Regex(@"^[A-Za-z][0-9]+$");
        static Regex _rxBrackets = new Regex(@"\[[^\]]*\]");
        static Regex _rxFraction = new Regex(@" \?+/[0-9\?]+$");

        // convert .NET format string into Excel format string
        static string FormatDotNetToXL(string fmt, Type dataType)
        {
            // no format?
            // return "d" for DateTime, empty for all others
            if (fmt == null || fmt.Length == 0)
            {
                return (dataType == typeof(DateTime))
                    ? "d"
                    : string.Empty;
            }

            // date formats are OK only if the dataType is null/object or DateTime
            bool dateOK = (dataType == null ||
                dataType == typeof(object) ||
                dataType == typeof(DateTime));

            // get localized formatting patterns
            NumberFormatInfo nfi = CultureInfo.CurrentUICulture.NumberFormat;
            DateTimeFormatInfo dtfi = CultureInfo.CurrentUICulture.DateTimeFormat;
            int digits = nfi.NumberDecimalDigits;
            if (fmt.StartsWith("c") || fmt.StartsWith("C"))
            {
                digits = nfi.CurrencyDecimalDigits;
            }

            // parse formats of style F4, D2, etc.
            if (_rxCharDigits.Match(fmt).Success)
            {
                try
                {
                    digits = int.Parse(fmt.Substring(1), CultureInfo.InvariantCulture);
                    fmt = fmt.Substring(0, 1);
                    dateOK = false; // <<B183>>
                }
                catch { }
            }

            // build format for decimal digits
            string strDigits = string.Empty;
            if (digits > 0)
            {
                strDigits = "." + new string('0', digits);
            }

            // convert standard .NET formats
            switch (fmt)
            {
                // currency									
                case "c":
                case "C":
                    return GetCurrencyFormat(nfi, strDigits);

                // numeric
                case "n":
                case "N":
                    return string.Format("#,##0{0}", strDigits);

                // percent
                case "p":
                case "P":
                    return string.Format("0{0}%", strDigits);

                // scientific <<B319>> VNFLX000715
                case "e":
                case "E":
                    return strDigits.Length > 0
                        ? string.Format("0{0}E+000", strDigits)
                        : "000000E+000";

                // hexadecimal (not supported in Excel) <<B319>>
                case "x":
                case "X":
                    return string.Empty;
            }

            // convert standard .NET formats for non-date values
            if (!dateOK)
            {
                switch (fmt)
                {
                    // decimal
                    case "D":
                    case "d":
                        return new string('0', Math.Max(1, digits));

                    // fixed point
                    case "f":
                    case "F":
                        return string.Format("0{0}", strDigits);

                    // general number
                    case "g":
                    case "G":
                        return string.Empty;
                }
            }

            // convert standard .NET formats for date values
            if (dateOK)
            {
                switch (fmt)
                {
                    // long date
                    case "D":
                        fmt = dtfi.LongDatePattern;
                        break;

                    // short date
                    case "d":
                        fmt = dtfi.ShortDatePattern;
                        break;

                    // short time
                    case "t":
                        fmt = dtfi.ShortTimePattern;
                        break;

                    // long time
                    case "T":
                        fmt = dtfi.LongTimePattern;
                        break;

                    // full date/time pattern (short time)
                    case "f":
                        fmt = string.Format("{0} {1}", dtfi.LongDatePattern, dtfi.ShortTimePattern);
                        break;

                    // fixed point/full date/time pattern (long time)
                    case "F":
                        fmt = string.Format("{0} {1}", dtfi.LongDatePattern, dtfi.LongTimePattern);
                        break;

                    // general date/time pattern (short time)
                    case "g":
                        fmt = string.Format("{0} {1}", dtfi.ShortDatePattern, dtfi.ShortTimePattern);
                        break;

                    // general date/time pattern (long time)
                    case "G":
                        fmt = string.Format("{0} {1}", dtfi.ShortDatePattern, dtfi.LongTimePattern);
                        break;

                    // month-day pattern
                    case "m":
                    case "M":
                        fmt = dtfi.MonthDayPattern;
                        break;

                    // month-day pattern
                    case "r":
                    case "R":
                        fmt = dtfi.RFC1123Pattern;
                        break;

                    // sortable date/time pattern; conforms to ISO 8601
                    case "s":
                        fmt = dtfi.SortableDateTimePattern;
                        break;

                    // universal sortable date/time pattern
                    case "u":
                        fmt = dtfi.UniversalSortableDateTimePattern;
                        break;

                    // universal sortable date/time pattern
                    case "U":
                        fmt = dtfi.FullDateTimePattern;
                        break;

                    // year-month pattern
                    case "y":
                    case "Y":
                        fmt = dtfi.YearMonthPattern;
                        break;
                }

                // fall through to convert quotes, locale, etc... <<B27>>
            }

            // convert custom date/time formats from .NET to Excel
            if (dateOK)
            {
                // convert custom time formats from .NET to Excel
                string fmtLower = fmt.ToLower();
                if (IndexOfAnyUnquoted(fmtLower, _timeChars) > -1)
                {
                    // handle hours, minutes, seconds, fractions
                    fmt = ReplaceUnquoted(fmt, ".ff", ".00");
                    fmt = ReplaceUnquoted(fmt, ".f", ".0");

                    // handle am/pm
                    if (fmt.IndexOf('t') > -1)
                    {
                        fmt = ReplaceUnquoted(fmt, "h", "H");
                    }
                }

                // convert custom date formats from .NET to Excel
                if (IndexOfAnyUnquoted(fmtLower, _dateChars) > -1)
                {
                    fmt = ReplaceUnquoted(fmt, "M", "m");
                    fmt = ReplaceUnquoted(fmt, "d", "D");
                    fmt = ReplaceUnquoted(fmt, "y", "Y");
                }

                // .NET uses tt for am/pm; Excel uses AM/PM
                fmt = ReplaceUnquoted(fmt, "tt", "AM/PM");
                fmt = ReplaceUnquoted(fmt, "t", "A/P");
            }

            // .NET uses single quotes for literals, Excel uses double <<B20>>
            fmt = fmt.Replace("'", "\"");

            // done
            return fmt;
        }

        // convert Excel format string into .NET format string
        static string FormatXLToDotNet(string fmt)
        {
            // empty? no work
            if (fmt == null || fmt.Length == 0) return fmt;

            // convert numeric formats from Excel to .NET
            fmt = _rxCurrency.Replace(fmt, "'$1'");		// foreign currency
            fmt = _rxBrackets.Replace(fmt, string.Empty); // e.g [Red]
            fmt = _rxFraction.Replace(fmt, ".####");		// e.g. ?/2
            fmt = ReplaceUnquoted(fmt, "*", string.Empty);
            fmt = ReplaceUnquoted(fmt, "\\", string.Empty);
            fmt = ReplaceUnquoted(fmt, "_(", string.Empty);
            fmt = ReplaceUnquoted(fmt, "_)", string.Empty);
            fmt = ReplaceUnquoted(fmt, "_-", string.Empty);	// <<B20>>
            fmt = ReplaceUnquoted(fmt, "_", string.Empty);	// <<B20>>
            fmt = ReplaceUnquoted(fmt, "@", string.Empty);	// <<B20>>

            // convert time formats from Excel to .NET
            string fmtLower = fmt.ToLower();
            if (IndexOfAnyUnquoted(fmtLower, _timeChars) > -1)
            {
                // handle hours, seconds, fractions
                fmt = ReplaceUnquoted(fmt, "h", "H");
                fmt = ReplaceUnquoted(fmt, "S", "s");
                fmt = ReplaceUnquoted(fmt, ".00", ".ff");
                fmt = ReplaceUnquoted(fmt, ".0", ".f");

                // if there are any dates, be conservative when converting minutes
                if (IndexOfAnyUnquoted(fmtLower, _dateChars) > -1)
                {
                    fmt = ReplaceUnquoted(fmt, ":MM", ":mm");
                    fmt = ReplaceUnquoted(fmt, ":M", ":m");
                }
                else
                {
                    fmt = ReplaceUnquoted(fmt, "M", "m");
                }

                // handle am/pm
                fmt = ReplaceUnquoted(fmt, "am/pm", "tt", true);
                fmt = ReplaceUnquoted(fmt, "a/p", "t", true);
                if (fmt.IndexOf('t') > -1)
                {
                    fmt = ReplaceUnquoted(fmt, "H", "h");
                }

                // dates/times don't have multi-part formats in .NET <<B20>>
                int pos = fmt.IndexOf(';');
                if (pos > -1)
                    fmt = fmt.Substring(0, pos);
            }

            // convert date formats from Excel to .NET
            if (IndexOfAnyUnquoted(fmtLower, _dateChars) > -1)
            {
                // handle days, years
                fmt = ReplaceUnquoted(fmt, "m", "M");
                fmt = ReplaceUnquoted(fmt, "D", "d");
                fmt = ReplaceUnquoted(fmt, "Y", "y");

                // if there are any times, be conservative when converting months
                if (IndexOfAnyUnquoted(fmtLower, _timeChars) > -1)
                {
                    fmt = ReplaceUnquoted(fmt, ":MM", ":mm");
                    fmt = ReplaceUnquoted(fmt, ":M", ":m");
                }

                // dates/times don't have multi-part formats in .NET <<B20>>
                int pos = fmt.IndexOf(';');
                if (pos > -1)
                    fmt = fmt.Substring(0, pos);
            }

            // Excel uses double quotes for literals, .NET uses single <<B20>>
            fmt = fmt.Replace("\"", "'");

            // done
            return fmt;
        }

        // build a localized excel currency format based on a .NET currency format <<IP48>>
        static string GetCurrencyFormat(NumberFormatInfo nfi, string strDigits)
        {
            // return value
            string fmt = string.Empty;

            // number (with digits)
            string number = (nfi.CurrencyGroupSizes.Length > 0 && nfi.CurrencyGroupSizes[0] > 0)
                ? "#,##0"
                : "0";
            number += strDigits;

            // currency sign positive pattern
            switch (nfi.CurrencyPositivePattern)
            {
                case 1: fmt = number + "$;"; break;
                case 2: fmt = "$ " + number + ";"; break;
                case 3: fmt = number + " $;"; break;
                default: fmt = "$" + number + ";"; break;
            }

            // currency sign positive pattern
            switch (nfi.CurrencyNegativePattern)
            {
                case 1: fmt = fmt + "-$" + number; break;
                case 2: fmt = fmt + "$-" + number; break;
                case 3: fmt = fmt + "$" + number + "-"; break;
                case 4: fmt = fmt + "(" + number + "$)"; break;
                case 5: fmt = fmt + "-" + number + "$"; break;
                case 6: fmt = fmt + number + "-$"; break;
                case 7: fmt = fmt + number + "$-"; break;
                case 8: fmt = fmt + "-" + number + " $"; break;
                case 9: fmt = fmt + "-$ " + number; break;
                case 10: fmt = fmt + number + " $-"; break;
                case 11: fmt = fmt + "$ " + number + "-"; break;
                case 12: fmt = fmt + "$ -" + number; break;
                case 13: fmt = fmt + number + "- $"; break;
                case 14: fmt = fmt + "($ " + number + ")"; break;
                case 15: fmt = fmt + "(" + number + " $)"; break;
                default: fmt = fmt + "($" + number + ")"; break;
            }

            // localize currency symbol <<B220>>
            if (nfi.CurrencySymbol != "$")
            {
                fmt = fmt.Replace("$", "\"" + nfi.CurrencySymbol + "\"");
            }

            // return localized currency format
            return fmt;
        }

        // similar to string.IndexOfAny, but handles quotes and escapes
        static int IndexOfAnyUnquoted(string s, params char[] chars)
        {
            // start unquoted/unescaped
            bool dquote = false;
            bool squote = false;
            bool escape = false;
            bool bracket = false;

            // scan string
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];

                // check if we're unquoted
                bool ok = !dquote && !squote && !escape && !bracket;

                // look for chars at this position
                if (ok)
                {
                    foreach (char cc in chars)
                    {
                        if (c == cc)
                            return i;
                    }
                }

                // manage escape char
                escape = (!escape && c == '\\');

                // manage quotes
                if (c == '\'' && !dquote)
                    squote = !squote;
                if (c == '\"' && !squote)
                    dquote = !dquote;

                // manage bracket
                if (c == '[' && !dquote && !squote)
                    bracket = true;
                if (c == ']' && !dquote && !squote)
                    bracket = false;
            }

            // not found
            return -1;
        }

        // similar to string.Replace, but handles quotes and escapes
        static string ReplaceUnquoted(string s, string find, string replace)
        {
            return ReplaceUnquoted(s, find, replace, false);
        }

        static string ReplaceUnquoted(string s, string find, string replace, bool ignoreCase)
        {
            // save time if we can
            if (!ignoreCase && s.IndexOf(find) < 0)
                return s;

            // start unquoted/unescaped
            bool dquote = false;
            bool squote = false;
            bool escape = false;

            // scan string
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];

                // check if we're unquoted
                bool ok = !dquote && !squote && !escape;

                // replace or append
                int len = find.Length;
                if (ok && string.Compare(s, i, find, 0, len, ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture) == 0)
                {
                    i += len - 1;			// skip rest of find string
                    sb.Append(replace);		// append replacement
                    continue;
                }

                // append current char
                sb.Append(c);

                // manage escape char
                escape = (!escape && c == '\\');

                // manage quotes
                if (c == '\'' && !dquote)
                    squote = !squote;
                if (c == '\"' && !squote)
                    dquote = !dquote;
            }

            // done
            return sb.ToString();
        }

        #endregion
    }
}
