//------------------------------------------------------------------------------
// <copyright file="XsdDuration.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <owner current="true" primary="true">[....]</owner>                                                                
//------------------------------------------------------------------------------

namespace System.Xml.Schema {
    using System;
    using System.Xml;
    using System.Diagnostics;
    using System.Text;

    /// <summary>
    /// This enum specifies what format should be used when converting string to XsdDateTime
    /// </summary>
    [Flags]
    internal enum XsdDateTimeFlags {
        DateTime        = 0x01,
        Time            = 0x02,
        Date            = 0x04,
        GYearMonth      = 0x08, 
        GYear           = 0x10, 
        GMonthDay       = 0x20, 
        GDay            = 0x40, 
        GMonth          = 0x80,
#if !SILVERLIGHT // XDR is not supported in Silverlight
        XdrDateTimeNoTz = 0x100,
        XdrDateTime     = 0x200,
        XdrTimeNoTz     = 0x400,  //XDRTime with tz is the same as xsd:time  
#endif
        AllXsd          = 0xFF //All still does not include the XDR formats
    }

    /// <summary>
    /// This structure extends System.DateTime to support timeInTicks zone and Gregorian types scomponents of an Xsd Duration.  It is used internally to support Xsd durations without loss
    /// of fidelity.  XsdDuration structures are immutable once they've been created.
    /// </summary>
    internal struct XsdDateTime {
        // DateTime is being used as an internal representation only
        // Casting XsdDateTime to DateTime might return a different value
        private DateTime dt;

        // Additional information that DateTime is not preserving
        // Information is stored in the following format:
        // Bits     Info
        // 31-24    DateTimeTypeCode 
        // 23-16    XsdDateTimeKind
        // 15-8     Zone Hours
        // 7-0      Zone Minutes
        private uint extra;


        // Subset of XML Schema types XsdDateTime represents
        enum DateTimeTypeCode {
            DateTime,
            Time,
            Date,
            GYearMonth,
            GYear,
            GMonthDay,
            GDay,
            GMonth,
#if !SILVERLIGHT // XDR is not supported in Silverlight
            XdrDateTime,
#endif
        }

        // Internal representation of DateTimeKind
        enum XsdDateTimeKind {
            Unspecified,
            Zulu,
            LocalWestOfZulu,    // GMT-1..14, N..Y
            LocalEastOfZulu     // GMT+1..14, A..M
        }

        // Masks and shifts used for packing and unpacking extra 
        private const uint TypeMask = 0xFF000000;
        private const uint KindMask = 0x00FF0000;
        private const uint ZoneHourMask = 0x0000FF00;
        private const uint ZoneMinuteMask = 0x000000FF;
        private const int TypeShift = 24;
        private const int KindShift = 16;
        private const int ZoneHourShift = 8;

        // Maximum number of fraction digits;
        private const short maxFractionDigits = 7;

        static readonly int Lzyyyy         = "yyyy".Length;
        static readonly int Lzyyyy_        = "yyyy-".Length;
        static readonly int Lzyyyy_MM      = "yyyy-MM".Length;
        static readonly int Lzyyyy_MM_     = "yyyy-MM-".Length;
        static readonly int Lzyyyy_MM_dd   = "yyyy-MM-dd".Length;
        static readonly int Lzyyyy_MM_ddT  = "yyyy-MM-ddT".Length;
        static readonly int LzHH           = "HH".Length;
        static readonly int LzHH_          = "HH:".Length;
        static readonly int LzHH_mm        = "HH:mm".Length;
        static readonly int LzHH_mm_       = "HH:mm:".Length;
        static readonly int LzHH_mm_ss     = "HH:mm:ss".Length;
        static readonly int Lz_            = "-".Length;
        static readonly int Lz_zz          = "-zz".Length;
        static readonly int Lz_zz_         = "-zz:".Length;
        static readonly int Lz_zz_zz       = "-zz:zz".Length;
        static readonly int Lz__           = "--".Length;
        static readonly int Lz__mm         = "--MM".Length;
        static readonly int Lz__mm_        = "--MM-".Length;
        static readonly int Lz__mm__       = "--MM--".Length;
        static readonly int Lz__mm_dd      = "--MM-dd".Length;
        static readonly int Lz___          = "---".Length;
        static readonly int Lz___dd        = "---dd".Length;


#if !SILVERLIGHT
        /// <summary>
        /// Constructs an XsdDateTime from a string trying all possible formats.
        /// </summary>
        public XsdDateTime(string text) : this(text, XsdDateTimeFlags.AllXsd) {
        }
#endif

        /// <summary>
        /// Constructs an XsdDateTime from a string using specific format.
        /// </summary>
        public XsdDateTime(string text, XsdDateTimeFlags kinds) : this() {
            Parser parser = new Parser();
            if (! parser.Parse(text, kinds)) {
                throw new FormatException(Res.GetString(Res.XmlConvert_BadFormat, text, kinds));
            }
            InitiateXsdDateTime(parser);
        }

#if !SILVERLIGHT
        private XsdDateTime(Parser parser) : this() {
            InitiateXsdDateTime(parser);
        }
#endif

        private void InitiateXsdDateTime(Parser parser) {
            dt = new DateTime(parser.year, parser.month, parser.day, parser.hour, parser.minute, parser.second);
            if (parser.fraction != 0) {
                dt = dt.AddTicks(parser.fraction);
            }
            extra = (uint)(((int)parser.typeCode << TypeShift) | ((int)parser.kind << KindShift) | (parser.zoneHour << ZoneHourShift) | parser.zoneMinute);
        }

#if !SILVERLIGHT
        internal static bool TryParse(string text, XsdDateTimeFlags kinds, out XsdDateTime result) {
            Parser parser = new Parser();
            if (! parser.Parse(text, kinds)) {
                result = new XsdDateTime();
                return false;
            }
            result = new XsdDateTime(parser);
            return true;
        }
#endif

        /// <summary>
        /// Constructs an XsdDateTime from a DateTime.
        /// </summary>
        public XsdDateTime(DateTime dateTime, XsdDateTimeFlags kinds) {
            Debug.Assert(Bits.ExactlyOne((uint)kinds), "Only one DateTime type code can be set.");
            dt = dateTime;

            DateTimeTypeCode code = (DateTimeTypeCode) (Bits.LeastPosition((uint) kinds) - 1);
            int zoneHour = 0;
            int zoneMinute = 0;
            XsdDateTimeKind kind;

            switch (dateTime.Kind) {
                case DateTimeKind.Unspecified: kind = XsdDateTimeKind.Unspecified; break;
                case DateTimeKind.Utc: kind = XsdDateTimeKind.Zulu; break;

                default: {
                    Debug.Assert(dateTime.Kind == DateTimeKind.Local, "Unknown DateTimeKind: " + dateTime.Kind);
                    TimeSpan utcOffset = TimeZoneInfo.Local.GetUtcOffset(dateTime);

                    if (utcOffset.Ticks < 0) {
                        kind = XsdDateTimeKind.LocalWestOfZulu;
                        zoneHour = -utcOffset.Hours;
                        zoneMinute = -utcOffset.Minutes;
                    }
                    else {
                        kind = XsdDateTimeKind.LocalEastOfZulu;
                        zoneHour = utcOffset.Hours;
                        zoneMinute = utcOffset.Minutes;
                    }
                    break;
                }
            }

            extra = (uint)(((int)code << TypeShift) | ((int)kind << KindShift) | (zoneHour << ZoneHourShift) | zoneMinute);
        }

        // Constructs an XsdDateTime from a DateTimeOffset
        public XsdDateTime(DateTimeOffset dateTimeOffset) : this(dateTimeOffset, XsdDateTimeFlags.DateTime) {
        }

        public XsdDateTime(DateTimeOffset dateTimeOffset, XsdDateTimeFlags kinds) {
            Debug.Assert(Bits.ExactlyOne((uint)kinds), "Only one DateTime type code can be set.");

            dt = dateTimeOffset.DateTime;

            TimeSpan zoneOffset = dateTimeOffset.Offset;
            DateTimeTypeCode code = (DateTimeTypeCode) (Bits.LeastPosition((uint) kinds) - 1);
            XsdDateTimeKind kind;
            if (zoneOffset.TotalMinutes < 0) {
                zoneOffset = zoneOffset.Negate();
                kind = XsdDateTimeKind.LocalWestOfZulu;
            }
            else if (zoneOffset.TotalMinutes > 0) {
                kind = XsdDateTimeKind.LocalEastOfZulu;
            }
            else {
                kind = XsdDateTimeKind.Zulu;
            }

            extra = (uint)(((int)code << TypeShift) | ((int)kind << KindShift) | (zoneOffset.Hours << ZoneHourShift) | zoneOffset.Minutes);
        }

        /// <summary>
        /// Returns auxiliary enumeration of XSD date type
        /// </summary>
        private DateTimeTypeCode InternalTypeCode {
            get { return (DateTimeTypeCode)((extra & TypeMask) >> TypeShift); }
        }

        /// <summary>
        /// Returns geographical "position" of the value
        /// </summary>
        private XsdDateTimeKind InternalKind {
            get { return (XsdDateTimeKind)((extra & KindMask) >> KindShift); }
        }

#if !SILVERLIGHT
        /// <summary>
        /// Returns XmlTypeCode of the value being stored
        /// </summary>
        public XmlTypeCode TypeCode {
            get { return typeCodes[(int)InternalTypeCode]; }
        }

        /// <summary>
        /// Returns whether object represent local, UTC or unspecified time
        /// </summary>
        public DateTimeKind Kind {
            get {
                switch (InternalKind) {
                case XsdDateTimeKind.Unspecified:
                    return DateTimeKind.Unspecified;
                case XsdDateTimeKind.Zulu:
                    return DateTimeKind.Utc;
                default:
                    // XsdDateTimeKind.LocalEastOfZulu:
                    // XsdDateTimeKind.LocalWestOfZulu:
                    return DateTimeKind.Local;
                }
            }
        }
#endif

        /// <summary>
        /// Returns the year part of XsdDateTime
        /// The returned value is integer between 1 and 9999
        /// </summary>
        public int Year {
            get { return dt.Year; }
        }
 
        /// <summary>
        /// Returns the month part of XsdDateTime
        /// The returned value is integer between 1 and 12
        /// </summary>
        public int Month {
            get { return dt.Month; }
        }
 
        /// <summary>
        /// Returns the day of the month part of XsdDateTime
        /// The returned value is integer between 1 and 31
        /// </summary>
        public int Day {
            get { return dt.Day; }
        }
 
        /// <summary>
        /// Returns the hour part of XsdDateTime
        /// The returned value is integer between 0 and 23
        /// </summary>
        public int Hour {
            get { return dt.Hour; }
        }
 
        /// <summary>
        /// Returns the minute part of XsdDateTime
        /// The returned value is integer between 0 and 60
        /// </summary>
        public int Minute {
            get { return dt.Minute; }
        }
 
        /// <summary>
        /// Returns the second part of XsdDateTime
        /// The returned value is integer between 0 and 60
        /// </summary>
        public int Second {
            get { return dt.Second; }
        }

        /// <summary>
        /// Returns number of ticks in the fraction of the second
        /// The returned value is integer between 0 and 9999999
        /// </summary>
        public int Fraction {
            get { return (int)(dt.Ticks - new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second).Ticks); }
        }
 
        /// <summary>
        /// Returns the hour part of the time zone
        /// The returned value is integer between -13 and 13
        /// </summary>
        public int ZoneHour {
            get { 
                uint result = (extra & ZoneHourMask) >> ZoneHourShift; 
                return (int)result;
            }
        }

        /// <summary>
        /// Returns the minute part of the time zone
        /// The returned value is integer between 0 and 60
        /// </summary>
        public int ZoneMinute {
            get { 
                uint result = (extra & ZoneMinuteMask); 
                return (int)result;
            }
        }

#if !SILVERLIGHT
        public DateTime ToZulu() {
            switch (InternalKind) {
                case XsdDateTimeKind.Zulu:
                    // set it to UTC
                    return new DateTime(dt.Ticks, DateTimeKind.Utc);
                case XsdDateTimeKind.LocalEastOfZulu:
                    // Adjust to UTC and then convert to local in the current time zone
                    return new DateTime(dt.Subtract(new TimeSpan(ZoneHour, ZoneMinute, 0)).Ticks, DateTimeKind.Utc);
                case XsdDateTimeKind.LocalWestOfZulu:
                    // Adjust to UTC and then convert to local in the current time zone
                    return new DateTime(dt.Add(new TimeSpan(ZoneHour, ZoneMinute, 0)).Ticks, DateTimeKind.Utc); 
                default:
                    return dt;
            }
        }
#endif

        /// <summary>
        /// Cast to DateTime
        /// The following table describes the behaviors of getting the default value
        /// when a certain year/month/day values are missing.
        /// 
        /// An "X" means that the value exists.  And "--" means that value is missing.
        /// 
        /// Year    Month   Day =>  ResultYear  ResultMonth     ResultDay       Note
        /// 
        /// X       X       X       Parsed year Parsed month    Parsed day
        /// X       X       --      Parsed Year Parsed month    First day       If we have year and month, assume the first day of that month.
        /// X       --      X       Parsed year First month     Parsed day      If the month is missing, assume first month of that year.
        /// X       --      --      Parsed year First month     First day       If we have only the year, assume the first day of that year.
        /// 
        /// --      X       X       CurrentYear Parsed month    Parsed day      If the year is missing, assume the current year.
        /// --      X       --      CurrentYear Parsed month    First day       If we have only a month value, assume the current year and current day.
        /// --      --      X       CurrentYear First month     Parsed day      If we have only a day value, assume current year and first month.
        /// --      --      --      CurrentYear Current month   Current day     So this means that if the date string only contains time, you will get current date.
        /// </summary>
        public static implicit operator DateTime(XsdDateTime xdt) {
            DateTime result;
            switch (xdt.InternalTypeCode) {
                case DateTimeTypeCode.GMonth:
                case DateTimeTypeCode.GDay:
                    result = new DateTime(DateTime.Now.Year, xdt.Month, xdt.Day);
                    break;
                case DateTimeTypeCode.Time:
                    //back to DateTime.Now 
                    DateTime currentDateTime = DateTime.Now;
                    TimeSpan addDiff = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day) - new DateTime(xdt.Year, xdt.Month, xdt.Day);
                    result = xdt.dt.Add(addDiff);
                    break;
                default:
                    result = xdt.dt;
                    break;
            }

            long ticks;
            switch (xdt.InternalKind) {
                case XsdDateTimeKind.Zulu:
                    // set it to UTC
                    result = new DateTime(result.Ticks, DateTimeKind.Utc);
                    break;
                case XsdDateTimeKind.LocalEastOfZulu:
                    // Adjust to UTC and then convert to local in the current time zone
                    ticks = result.Ticks - new TimeSpan(xdt.ZoneHour, xdt.ZoneMinute, 0).Ticks;
                    if (ticks < DateTime.MinValue.Ticks)
                    {
                        // Underflow. Return the DateTime as local time directly
                        ticks += TimeZoneInfo.Local.GetUtcOffset(result).Ticks;
                        if (ticks < DateTime.MinValue.Ticks)
                            ticks = DateTime.MinValue.Ticks;
                        return new DateTime(ticks, DateTimeKind.Local);
                    }
                    result = new DateTime(ticks, DateTimeKind.Utc).ToLocalTime();
                    break;
                case XsdDateTimeKind.LocalWestOfZulu:
                    // Adjust to UTC and then convert to local in the current time zone
                    ticks = result.Ticks + new TimeSpan(xdt.ZoneHour, xdt.ZoneMinute, 0).Ticks;
                    if (ticks > DateTime.MaxValue.Ticks)
                    {
                        // Overflow. Return the DateTime as local time directly
                        ticks += TimeZoneInfo.Local.GetUtcOffset(result).Ticks;
                        if (ticks > DateTime.MaxValue.Ticks)
                            ticks = DateTime.MaxValue.Ticks;
                        return new DateTime(ticks, DateTimeKind.Local);
                    }
                    result = new DateTime(ticks, DateTimeKind.Utc).ToLocalTime(); 
                    break;
                default:
                    break;
            }
            return result;
        }

        public static implicit operator DateTimeOffset(XsdDateTime xdt) {
            DateTime dt;

            switch (xdt.InternalTypeCode) {
                case DateTimeTypeCode.GMonth:
                case DateTimeTypeCode.GDay:
                    dt = new DateTime( DateTime.Now.Year, xdt.Month, xdt.Day );
                    break;
                case DateTimeTypeCode.Time:
                    //back to DateTime.Now 
                    DateTime currentDateTime = DateTime.Now;
                    TimeSpan addDiff = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day) - new DateTime(xdt.Year, xdt.Month, xdt.Day);
                    dt = xdt.dt.Add( addDiff );
                    break;
                default:
                    dt = xdt.dt;
                    break;
            }

            DateTimeOffset result;
            switch (xdt.InternalKind) {
                case XsdDateTimeKind.LocalEastOfZulu:
                    result = new DateTimeOffset(dt, new TimeSpan(xdt.ZoneHour, xdt.ZoneMinute, 0));
                    break;
                case XsdDateTimeKind.LocalWestOfZulu:
                    result = new DateTimeOffset(dt, new TimeSpan(-xdt.ZoneHour, -xdt.ZoneMinute, 0));
                    break;
                case XsdDateTimeKind.Zulu:
                    result = new DateTimeOffset(dt, new TimeSpan( 0 ) );
                    break;
                case XsdDateTimeKind.Unspecified:
                default:
                    result = new DateTimeOffset(dt, TimeZoneInfo.Local.GetUtcOffset(dt));
                    break;
            }

            return result;
        }

#if !SILVERLIGHT
        /// <summary>
        /// Compares two DateTime values, returning an integer that indicates
        /// their relationship.
        /// </summary>
        public static int Compare(XsdDateTime left, XsdDateTime right) {
            if (left.extra == right.extra) {
                return DateTime.Compare(left.dt, right.dt);
            }
            else {
                // Xsd types should be the same for it to be comparable
                if (left.InternalTypeCode != right.InternalTypeCode) {
                    throw new ArgumentException(Res.GetString(Res.Sch_XsdDateTimeCompare, left.TypeCode, right.TypeCode));
                }
                // Convert both to UTC
                return DateTime.Compare(left.GetZuluDateTime(), right.GetZuluDateTime());

            }
        }

        // Compares this DateTime to a given object. This method provides an
        // implementation of the IComparable interface. The object
        // argument must be another DateTime, or otherwise an exception
        // occurs.  Null is considered less than any instance.
        //
        // Returns a value less than zero if this  object
        /// <include file='doc\DateTime.uex' path='docs/doc[@for="DateTime.CompareTo"]/*' />
        public int CompareTo(Object value) {
            if (value == null) return 1;
            return Compare(this, (XsdDateTime)value);
        }
#endif

        /// <summary>
        /// Serialization to a string
        /// </summary>
        public override string ToString() {
            StringBuilder sb = new StringBuilder(64);
            char[] text;
            switch (InternalTypeCode) {
            case DateTimeTypeCode.DateTime:
                PrintDate(sb);
                sb.Append('T');
                PrintTime(sb);
                break;
            case DateTimeTypeCode.Time:
                PrintTime(sb);
                break;
            case DateTimeTypeCode.Date:
                PrintDate(sb);
                break;
            case DateTimeTypeCode.GYearMonth:
                text = new char[Lzyyyy_MM];
                IntToCharArray(text, 0, Year, 4);
                text[Lzyyyy] = '-';
                ShortToCharArray(text, Lzyyyy_, Month);
                sb.Append(text);
                break;
            case DateTimeTypeCode.GYear:
                text = new char[Lzyyyy];
                IntToCharArray(text, 0, Year, 4);
                sb.Append(text);
                break;
            case DateTimeTypeCode.GMonthDay:
                text = new char[Lz__mm_dd];
                text[0] = '-';
                text[Lz_] = '-';
                ShortToCharArray(text, Lz__, Month);
                text[Lz__mm] = '-';
                ShortToCharArray(text, Lz__mm_, Day);
                sb.Append(text);
                break;
            case DateTimeTypeCode.GDay:
                text = new char[Lz___dd];
                text[0] = '-';
                text[Lz_] = '-';
                text[Lz__] = '-';
                ShortToCharArray(text, Lz___, Day);
                sb.Append(text);
                break;
            case DateTimeTypeCode.GMonth:
                text = new char[Lz__mm__];
                text[0] = '-';
                text[Lz_] = '-';
                ShortToCharArray(text, Lz__, Month);
                text[Lz__mm] = '-';
                text[Lz__mm_] = '-';
                sb.Append(text);
                break;
            }
            PrintZone(sb);
            return sb.ToString();
        }

        // Serialize year, month and day
        private void PrintDate(StringBuilder sb) {
            char[] text = new char[Lzyyyy_MM_dd];
            IntToCharArray(text, 0, Year, 4);
            text[Lzyyyy] = '-';
            ShortToCharArray(text, Lzyyyy_, Month);
            text[Lzyyyy_MM] = '-';
            ShortToCharArray(text, Lzyyyy_MM_, Day);
            sb.Append(text);
        }

        // Serialize hour, minute, second and fraction
        private void PrintTime(StringBuilder sb) {
            char[] text = new char[LzHH_mm_ss];
            ShortToCharArray(text, 0, Hour);
            text[LzHH] = ':';
            ShortToCharArray(text, LzHH_, Minute);
            text[LzHH_mm] = ':';
            ShortToCharArray(text, LzHH_mm_, Second);
            sb.Append(text);
            int fraction = Fraction;
            if (fraction != 0) {   
                int fractionDigits = maxFractionDigits;
                while (fraction % 10 == 0) {
                    fractionDigits --;
                    fraction /= 10;
                }
                text = new char[fractionDigits + 1];
                text[0] = '.';
                IntToCharArray(text, 1, fraction, fractionDigits);
                sb.Append(text);
            }
        }

        // Serialize time zone
        private void PrintZone(StringBuilder sb) {
            char[] text;
            switch (InternalKind) {
            case XsdDateTimeKind.Zulu: 
                sb.Append('Z');
                break;
            case XsdDateTimeKind.LocalWestOfZulu:
                text = new char[Lz_zz_zz];
                text[0] = '-';
                ShortToCharArray(text, Lz_, ZoneHour);
                text[Lz_zz] = ':';
                ShortToCharArray(text, Lz_zz_, ZoneMinute);
                sb.Append(text);
                break;
            case XsdDateTimeKind.LocalEastOfZulu:
                text = new char[Lz_zz_zz];
                text[0] = '+';
                ShortToCharArray(text, Lz_, ZoneHour);
                text[Lz_zz] = ':';
                ShortToCharArray(text, Lz_zz_, ZoneMinute);
                sb.Append(text);
                break;
            default:
                // do nothing
                break;
            }
        }

        // Serialize integer into character array starting with index [start]. 
        // Number of digits is set by [digits]
        private void IntToCharArray(char[] text, int start, int value, int digits) {
            while(digits -- != 0) {
                text[start + digits] = (char)(value%10 + '0');
                value /= 10;
            }
        }

        // Serialize two digit integer into character array starting with index [start].
        private void ShortToCharArray(char[] text, int start, int value) {
            text[start] = (char)(value/10 + '0');
            text[start + 1] = (char)(value%10 + '0');
        }

#if !SILVERLIGHT
        // Auxiliary for compare. 
        // Returns UTC DateTime
        private DateTime GetZuluDateTime() {
            switch (InternalKind) {
            case XsdDateTimeKind.Zulu:
                return dt;
            case XsdDateTimeKind.LocalEastOfZulu:
                return dt.Subtract(new TimeSpan(ZoneHour, ZoneMinute, 0));
            case XsdDateTimeKind.LocalWestOfZulu:
                return dt.Add(new TimeSpan(ZoneHour, ZoneMinute, 0));
            default:
                return dt.ToUniversalTime();
            }
        }
#endif

        private static readonly XmlTypeCode[] typeCodes = {
            XmlTypeCode.DateTime,
            XmlTypeCode.Time,
            XmlTypeCode.Date,
            XmlTypeCode.GYearMonth,
            XmlTypeCode.GYear,
            XmlTypeCode.GMonthDay,
            XmlTypeCode.GDay,
            XmlTypeCode.GMonth
        };


        // Parsing string according to XML schema spec
        struct Parser {
            private const int leapYear = 1904;
            private const int firstMonth = 1;
            private const int firstDay = 1;

            public DateTimeTypeCode typeCode;
            public int year;
            public int month;
            public int day;
            public int hour;
            public int minute;
            public int second;
            public int fraction;
            public XsdDateTimeKind kind;
            public int zoneHour;
            public int zoneMinute;

            private string text;
            private int length;

            public bool Parse(string text, XsdDateTimeFlags kinds) {
                this.text = text;
                this.length = text.Length;

                // Skip leading withitespace
                int start = 0;
                while(start < length && char.IsWhiteSpace(text[start])) {
                    start ++;
                }
                // Choose format starting from the most common and trying not to reparse the same thing too many times

#if !SILVERLIGHT // XDR is not supported in Silverlight
                if (Test(kinds, XsdDateTimeFlags.DateTime | XsdDateTimeFlags.Date | XsdDateTimeFlags.XdrDateTime | XsdDateTimeFlags.XdrDateTimeNoTz)) {
#else
                if (Test(kinds, XsdDateTimeFlags.DateTime | XsdDateTimeFlags.Date)) {
#endif
                    if (ParseDate(start)) {                                                  
                        if (Test(kinds, XsdDateTimeFlags.DateTime)) {
                            if (ParseChar(start + Lzyyyy_MM_dd, 'T') && ParseTimeAndZoneAndWhitespace(start + Lzyyyy_MM_ddT)) {
                                typeCode = DateTimeTypeCode.DateTime;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.Date)) {  
                            if (ParseZoneAndWhitespace(start + Lzyyyy_MM_dd)) {
                                typeCode = DateTimeTypeCode.Date;
                                return true;
                            }
                        }
#if !SILVERLIGHT // XDR is not supported in Silverlight
                        if (Test(kinds, XsdDateTimeFlags.XdrDateTime)) {
                            if (ParseZoneAndWhitespace(start + Lzyyyy_MM_dd) || (ParseChar(start + Lzyyyy_MM_dd, 'T') && ParseTimeAndZoneAndWhitespace(start + Lzyyyy_MM_ddT)) ) {
                                typeCode = DateTimeTypeCode.XdrDateTime;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.XdrDateTimeNoTz)) {
                            if (ParseChar(start + Lzyyyy_MM_dd, 'T')) {
                                if (ParseTimeAndWhitespace(start + Lzyyyy_MM_ddT)) {
                                    typeCode = DateTimeTypeCode.XdrDateTime;
                                    return true;
                                }
                            }
                            else {
                                typeCode = DateTimeTypeCode.XdrDateTime;
                                return true;
                            }
                        }
#endif
                    }
                }

                if (Test(kinds, XsdDateTimeFlags.Time)) {
                    if (ParseTimeAndZoneAndWhitespace(start)) { //Equivalent to NoCurrentDateDefault on DateTimeStyles while parsing xs:time
                        year = leapYear;
                        month = firstMonth;
                        day = firstDay;
                        typeCode = DateTimeTypeCode.Time;
                        return true;
                    }
                }

#if !SILVERLIGHT // XDR is not supported in Silverlight
                if (Test(kinds, XsdDateTimeFlags.XdrTimeNoTz)) {
                    if (ParseTimeAndWhitespace(start)) { //Equivalent to NoCurrentDateDefault on DateTimeStyles while parsing xs:time
                        year = leapYear;
                        month = firstMonth;
                        day = firstDay;
                        typeCode = DateTimeTypeCode.Time;
                        return true;
                    }
                }
#endif

                if (Test(kinds, XsdDateTimeFlags.GYearMonth | XsdDateTimeFlags.GYear)) {
                    if (Parse4Dig(start ,         ref year) && 1 <= year) {
                        if (Test(kinds, XsdDateTimeFlags.GYearMonth)) {
                            if (
                                ParseChar(start + Lzyyyy,     '-') &&
                                Parse2Dig(start + Lzyyyy_,    ref month) && 1 <= month && month <= 12 &&
                                ParseZoneAndWhitespace(start + Lzyyyy_MM)
                            ) {
                                day = firstDay;
                                typeCode = DateTimeTypeCode.GYearMonth;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.GYear)) {
                            if (ParseZoneAndWhitespace(start + Lzyyyy)) {
                                month = firstMonth;
                                day = firstDay;
                                typeCode = DateTimeTypeCode.GYear;
                                return true;
                            }
                        }                        
                    }
                }
                if (Test(kinds, XsdDateTimeFlags.GMonthDay | XsdDateTimeFlags.GMonth)) {
                    if (
                        ParseChar(start ,         '-') &&
                        ParseChar(start + Lz_,    '-') &&
                        Parse2Dig(start + Lz__,   ref month) && 1 <= month && month <= 12
                    ) {
                        if (Test(kinds, XsdDateTimeFlags.GMonthDay) && ParseChar(start + Lz__mm, '-')) {
                            if (
                                Parse2Dig(start + Lz__mm_,     ref day) && 1 <= day && day <= DateTime.DaysInMonth(leapYear, month) &&
                                ParseZoneAndWhitespace(start + Lz__mm_dd)
                            ) {
                                year = leapYear;
                                typeCode = DateTimeTypeCode.GMonthDay;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.GMonth)) {
                            if (ParseZoneAndWhitespace(start + Lz__mm) || (ParseChar(start + Lz__mm, '-') && ParseChar(start + Lz__mm_, '-') && ParseZoneAndWhitespace(start + Lz__mm__)) ) {
                                year = leapYear;
                                day = firstDay;
                                typeCode = DateTimeTypeCode.GMonth;
                                return true;
                            }
                        }
                    }
                
                }
                if (Test(kinds, XsdDateTimeFlags.GDay)) {
                    if (
                        ParseChar(start ,            '-') &&
                        ParseChar(start + Lz_,       '-') &&
                        ParseChar(start + Lz__,      '-') &&
                        Parse2Dig(start + Lz___,     ref day) && 1 <= day && day <= DateTime.DaysInMonth(leapYear, firstMonth) &&
                        ParseZoneAndWhitespace(start + Lz___dd)

                    ) {
                        year = leapYear;
                        month = firstMonth;
                        typeCode = DateTimeTypeCode.GDay;
                        return true;
                    }
                }
                return false;
            }


            private bool ParseDate(int start) {
                return  
                    Parse4Dig(start ,         ref year) && 1 <= year &&
                    ParseChar(start + Lzyyyy,     '-') &&
                    Parse2Dig(start + Lzyyyy_,    ref month) && 1 <= month && month <= 12 &&
                    ParseChar(start + Lzyyyy_MM,  '-') &&
                    Parse2Dig(start + Lzyyyy_MM_, ref day) && 1 <= day && day <= DateTime.DaysInMonth(year, month);
            }

            private bool ParseTimeAndZoneAndWhitespace(int start) {
                if (ParseTime(ref start)) {
                    if (ParseZoneAndWhitespace(start)) {
                        return true;
                    }
                }
                return false;
            }

#if !SILVERLIGHT // XDR is not supported in Silverlight
            private bool ParseTimeAndWhitespace(int start) {
                if (ParseTime(ref start)) {
                    while(start < length ) {//&& char.IsWhiteSpace(text[start])) {
                        start ++;
                    }
                    return start == length;    
                }
                return false;
            }
#endif

            static int[] Power10 = new int[maxFractionDigits] {-1, 10, 100, 1000, 10000, 100000, 1000000};
            private bool ParseTime(ref int start) {
                if (
                    Parse2Dig(start ,       ref hour) && hour < 24 &&
                    ParseChar(start + LzHH,     ':') &&
                    Parse2Dig(start + LzHH_,    ref minute) && minute < 60 &&
                    ParseChar(start + LzHH_mm,  ':') &&
                    Parse2Dig(start + LzHH_mm_, ref second) && second < 60
                ) {
                    start += LzHH_mm_ss; 
                    if (ParseChar(start, '.')) {
                        // Parse factional part of seconds
                        // We allow any number of digits, but keep only first 7
                        this.fraction = 0;
                        int fractionDigits = 0;
                        int round = 0;
                        while (++start < length) {
                            int d = text[start] - '0';
                            if (9u < (uint) d) { // d < 0 || 9 < d
                                break;
                            }
                            if (fractionDigits < maxFractionDigits) {
                                this.fraction = (this.fraction * 10) + d;
                            } else if (fractionDigits == maxFractionDigits) {
                                if (5 < d) {
                                    round = 1;
                                } else if (d == 5) {
                                    round = -1;
                                }
                            } else if (round < 0 && d != 0) {
                                round = 1;
                            }
                            fractionDigits ++;
                        }
                        if (fractionDigits < maxFractionDigits) {
                            if (fractionDigits == 0) {
                                return false; // cannot end with .
                            }
                            fraction *= Power10[maxFractionDigits - fractionDigits];
                        } else {
                            if (round < 0) {
                                round = fraction & 1;
                            }
                            fraction += round;
                        }
                    }
                    return true;
                }
                // cleanup - conflict with gYear
                hour = 0;
                return false;
            }

            private bool ParseZoneAndWhitespace(int start) {
                if (start < length) {
                    char ch = text[start]; 
                    if (ch == 'Z' || ch == 'z') {
                        kind = XsdDateTimeKind.Zulu;
                        start ++;
                    }
                    else if (start + 5 < length) {
                        if (
                            Parse2Dig(start + Lz_,       ref zoneHour) && zoneHour <= 99 &&
                            ParseChar(start + Lz_zz,     ':') &&
                            Parse2Dig(start + Lz_zz_,    ref zoneMinute) && zoneMinute <= 99
                        ) {
                            if (ch == '-') {
                                kind = XsdDateTimeKind.LocalWestOfZulu;
                                start += Lz_zz_zz;
                            }
                            else if (ch == '+') {
                                kind = XsdDateTimeKind.LocalEastOfZulu;
                                start += Lz_zz_zz;
                            }
                        }
                    }
                }
                while(start < length && char.IsWhiteSpace(text[start])) {
                    start ++;
                }
                return start == length;
            }


            private bool Parse4Dig(int start, ref int num) {
                if (start + 3 < length) {
                    int d4 = text[start]     - '0';
                    int d3 = text[start + 1] - '0';
                    int d2 = text[start + 2] - '0';
                    int d1 = text[start + 3] - '0';
                    if (0 <= d4 && d4 < 10 &&
                        0 <= d3 && d3 < 10 &&
                        0 <= d2 && d2 < 10 &&
                        0 <= d1 && d1 < 10
                    ) {
                        num = ((d4 * 10 + d3) * 10 + d2) * 10 + d1;
                        return true;
                    }
                }
                return false;
            }

            private bool Parse2Dig(int start, ref int num) {
                if (start + 1 < length) {
                    int d2 = text[start]     - '0';
                    int d1 = text[start + 1] - '0';
                    if (0 <= d2 && d2 < 10 &&
                        0 <= d1 && d1 < 10
                        ) {
                        num =  d2 * 10 + d1;
                        return true;
                    }
                }
                return false;
            }

            private bool ParseChar(int start, char ch) {
                return start < length && text[start] == ch;
            }

            private static bool Test(XsdDateTimeFlags left, XsdDateTimeFlags right) {
                return (left & right) != 0;
            }

        }
    }
}
