using System;
using System.Runtime.CompilerServices;

namespace CPF.Windows.Json.Deserialize
{
    internal class TimeSpanResolve : DefaultJsonResolve
    {
        [FuncLable(FuncType.SameType)]
#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        internal static unsafe TimeSpan ReadTimeSpan( JsonReader reader, JsonDeserializeHandler handler)
        {
            /*
             \"P10675199DT2H48M5.4775807S\"  => ISO
             \"10675199.02:48:05.4775807\"   => Microsoft
             */
            reader.ReadQuotes();
            char* ip = reader.Pointer;
            char c = ip[0];
            if (c == '-')
                c = ip[1];

            if (c == 'P')
                return _ReadISO8601TimeSpan(reader);
            else
                return ReadMicrosoftTimeSpan(reader);
        }

        static readonly ulong MinTicks = (ulong)-TimeSpan.MinValue.Ticks;
        static readonly ulong MaxTicks = (ulong)TimeSpan.MaxValue.Ticks;

        static readonly double[] DivideFractionBy =
          {
                10,
                100,
                1000,
                10000,
                100000,
                1000000,
                10000000,
                100000000
          };
#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        static unsafe TimeSpan ReadMicrosoftTimeSpan(JsonReader reader)
        {
            char* ip = reader.Pointer;
            int start = reader.Remaining;
            int strLen = -1;
            for (int z = 0; z < reader.Remaining; z++)
            {
                if (ip[z] == '"')
                {
                    strLen = z;
                    break;
                }
            }

            if (strLen < 1)
                throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan));

            reader.Pointer += strLen + 1;// "
            reader.Remaining -= strLen + 1;// "

            int days, hours, minutes, seconds, fraction;
            days = hours = minutes = seconds = fraction = 0;

            bool isNegative, pastDays, pastHours, pastMinutes, pastSeconds;
            isNegative = pastDays = pastHours = pastMinutes = pastSeconds = false;

            var ixOfLastPeriod = -1;
            var part = 0;

            int i;

            if (ip[0] == '-')
            {
                isNegative = true;
                i = 1;
            }
            else
            {
                i = 0;
            }

            for (; i < strLen; i++)
            {
                var c = ip[i];
                if (c == '.')
                {
                    ixOfLastPeriod = i;

                    if (!pastDays)
                    {
                        days = part;
                        part = 0;
                        pastDays = true;
                        continue;
                    }

                    if (!pastSeconds)
                    {
                        seconds = part;
                        part = 0;
                        pastSeconds = true;
                        continue;
                    }

                    throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan));
                }

                if (c == ':')
                {
                    if (!pastHours)
                    {
                        hours = part;
                        part = 0;
                        pastHours = true;
                        continue;
                    }

                    if (!pastMinutes)
                    {
                        minutes = part;
                        part = 0;
                        pastMinutes = true;
                        continue;
                    }

                    throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan));
                }

                if (c < '0' || c > '9')
                {
                    throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan));
                }

                part *= 10;
                part += c - '0';
            }

            if (!pastSeconds)
            {
                seconds = part;
                pastSeconds = true;
            }
            else
            {
                fraction = part;
            }

            if (!pastHours || !pastMinutes || !pastSeconds)
            {
                throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Missing required portion of TimeSpan");
            }

            var msInt = 0;
            if (fraction != 0)
            {
                var sizeOfFraction = strLen - (ixOfLastPeriod + 1);

                if (sizeOfFraction > 7)
                {
                    throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Fractional part of TimeSpan too large");
                }

                var fracOfSecond = part / DivideFractionBy[sizeOfFraction - 1];
                var ms = fracOfSecond * 1000.0;
                msInt = (int)ms;
                if (ms > msInt)
                    return TimeSpan.Parse(reader.SubString(reader.Length - start, strLen));
            }

            var ret = new TimeSpan(days, hours, minutes, seconds, msInt);
            if (isNegative)
            {
                ret = ret.Negate();
            }

            return ret;
        }

#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        static unsafe TimeSpan _ReadISO8601TimeSpan(JsonReader reader)
        {
            const ulong ticksPerDay = 864000000000;

            const ulong ticksPerWeek = ticksPerDay * 7;
            const ulong ticksPerMonth = ticksPerDay * 30;
            const ulong ticksPerYear = ticksPerDay * 365;

            // Format goes like so:
            // - (-)P(([n]Y)([n]M)([n]D))(T([n]H)([n]M)([n]S))
            // - P[n]W
            char* ip = reader.Pointer;
            int len = -1;
            for (int i = 0; i < reader.Remaining; i++)
            {
                if (ip[i] == '"')
                {
                    len = i;
                    break;
                }
            }

            if (len < 1)
                throw new JsonWrongCharacterException(reader);

            reader.Pointer += len + 1;
            reader.Remaining -= len + 1;

            var ix = 0;
            var isNegative = false;

            var c = ip[ix];
            if (c == '-')
            {
                isNegative = true;
                ix++;
            }

            if (ix >= len)
            {
                throw new JsonWrongCharacterException("Expected P, instead TimeSpan string ended");
            }

            c = ip[ix];
            if (c != 'P')
            {
                throw new JsonWrongCharacterException("Expected P, found " + c.ToString());
            }

            ix++;   // skip 'P'

            var hasTimePart = ISO8601TimeSpan_ReadDatePart(ip, len, ref ix, out long year, out long month, out long week, out long day);

            if (week != -1 && (year != -1 || month != -1 || day != -1))
            {
                throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Week part of TimeSpan defined along with one or more of year, month, or day");
            }

            if (week != -1 && hasTimePart)
            {
                throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "TimeSpans with a week defined cannot also have a time defined");
            }

            if (year == -1) year = 0;
            if (month == -1) month = 0;
            if (week == -1) week = 0;
            if (day == -1) day = 0;

            ulong timeTicks;

            if (hasTimePart)
            {
                ix++;   // skip 'T'
                ISO8601TimeSpan_ReadTimePart(ip, len, ref ix, out timeTicks);
            }
            else
            {
                timeTicks = 0;
            }

            ulong ticks = 0;
            if (year != 0)
            {
                ticks += ((ulong)year) * ticksPerYear;
            }

            if (month != 0)
            {
                // .NET (via XmlConvert) converts months to years
                // This isn't inkeeping with the spec, but of the bad choices... I choose this one
                var yearsFromMonths = ((ulong)month) / 12;
                var monthsAfterYears = ((ulong)month) % 12;
                ticks += yearsFromMonths * ticksPerYear + monthsAfterYears * ticksPerMonth;
            }

            if (week != 0)
            {
                // ISO8601 defines weeks as 7 days, so don't convert weeks to months or years (even if that may seem more sensible)
                ticks += ((ulong)week) * ticksPerWeek;
            }

            ticks += ((ulong)day) * ticksPerDay + timeTicks;

            if (ticks >= MaxTicks && !isNegative)
            {
                return TimeSpan.MaxValue;
            }

            if (ticks >= MinTicks && isNegative)
            {
                return TimeSpan.MinValue;
            }

            var ret = new TimeSpan((long)ticks);
            if (isNegative)
            {
                ret = ret.Negate();
            }



            return ret;
        }
#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        static unsafe bool ISO8601TimeSpan_ReadDatePart(char* ip, int strLen, ref int ix, out long year, out long month, out long week, out long day)
        {
            year = month = week = day = -1;

            bool yearSeen, monthSeen, weekSeen, daySeen;
            yearSeen = monthSeen = weekSeen = daySeen = false;

            while (ix != strLen)
            {
                if (ip[ix] == 'T')
                {
                    return true;
                }

                var part = ISO8601TimeSpan_ReadPart(ip, strLen, ref ix, out int whole, out int fraction, out int fracLen);

                if (fracLen != 0)
                {
                    throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Fractional values are not supported in the year, month, day, or week parts of an ISO8601 TimeSpan");
                }

                if (part == 'Y')
                {
                    if (yearSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Year part of TimeSpan seen twice");
                    }

                    if (monthSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Year part of TimeSpan seen after month already parsed");
                    }

                    if (daySeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Year part of TimeSpan seen after day already parsed");
                    }

                    year = whole;
                    yearSeen = true;
                    continue;
                }

                if (part == 'M')
                {
                    if (monthSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Month part of TimeSpan seen twice");
                    }

                    if (daySeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Month part of TimeSpan seen after day already parsed");
                    }

                    month = whole;
                    monthSeen = true;
                    continue;
                }

                if (part == 'W')
                {
                    if (weekSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Week part of TimeSpan seen twice");
                    }

                    week = whole;
                    weekSeen = true;
                    continue;
                }

                if (part == 'D')
                {
                    if (daySeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Day part of TimeSpan seen twice");
                    }

                    day = whole;
                    daySeen = true;
                    continue;
                }

                throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Expected Y, M, W, or D but found: " + part);
            }

            return false;
        }
#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        static unsafe char ISO8601TimeSpan_ReadPart(char* ip, int strLen, ref int ix, out int whole, out int fraction, out int fracLen)
        {
            var part = 0;
            while (true)
            {
                var c = ip[ix];

                if (c == '.' || c == ',')
                {
                    whole = part;
                    break;
                }

                ix++;
                if (c < '0' || c > '9' || ix == strLen)
                {
                    whole = part;
                    fraction = 0;
                    fracLen = 0;
                    return c;
                }

                part *= 10;
                part += c - '0';
            }

            var ixOfPeriod = ix;

            ix++;   // skip the '.' or ','
            part = 0;
            while (true)
            {
                var c = ip[ix];

                ix++;
                if (c < '0' || c > '9' || ix == strLen)
                {
                    fraction = part;
                    fracLen = ix - 1 - (ixOfPeriod + 1);
                    return c;
                }

                part *= 10;
                part += c - '0';
            }
        }
#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        static unsafe void ISO8601TimeSpan_ReadTimePart(char* str, int strLen, ref int ix, out ulong ticks)
        {
            const ulong ticksPerHour = 36000000000;
            const ulong ticksPerMinute = 600000000;
            const ulong ticksPerSecond = 10000000;

            ticks = 0;

            bool hourSeen, minutesSeen, secondsSeen;
            hourSeen = minutesSeen = secondsSeen = false;

            var fracSeen = false;

            while (ix != strLen)
            {
                if (fracSeen)
                {
                    throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Expected Time part of TimeSpan to end");
                }

                char part = ISO8601TimeSpan_ReadPart(str, strLen, ref ix, out int whole, out int fraction, out int fracLen);

                if (fracLen != 0)
                {
                    fracSeen = true;
                }

                if (part == 'H')
                {
                    if (hourSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Hour part of TimeSpan seen twice");
                    }

                    if (minutesSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Hour part of TimeSpan seen after minutes already parsed");
                    }

                    if (secondsSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Hour part of TimeSpan seen after seconds already parsed");
                    }

                    ticks += (ulong)whole * ticksPerHour + ISO8601TimeSpan_FractionToTicks(9, fraction * 36, fracLen);
                    hourSeen = true;
                    continue;
                }

                if (part == 'M')
                {
                    if (minutesSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Minute part of TimeSpan seen twice");
                    }

                    if (secondsSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Minute part of TimeSpan seen after seconds already parsed");
                    }

                    ticks += (ulong)whole * ticksPerMinute + ISO8601TimeSpan_FractionToTicks(8, fraction * 6, fracLen);
                    minutesSeen = true;
                    continue;
                }

                if (part == 'S')
                {
                    if (secondsSeen)
                    {
                        throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Seconds part of TimeSpan seen twice");
                    }

                    ticks += (ulong)whole * ticksPerSecond + ISO8601TimeSpan_FractionToTicks(7, fraction, fracLen);
                    secondsSeen = true;
                    continue;
                }

                throw new JsonDeserializationTypeResolutionException(typeof(TimeSpan), "Expected H, M, or S but found: " + part);
            }
        }
#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        static ulong ISO8601TimeSpan_FractionToTicks(int maxLen, int fraction, int fracLen)
        {
            if (fracLen == 0)
            {
                return 0;
            }

            if (fracLen > maxLen)
            {
                fraction /= (int)Pow10(fracLen - maxLen);
                fracLen = maxLen;
            }

            return (ulong)(fraction * Pow10(maxLen - fracLen));
        }
        static readonly long[] PowersOf10 = {
            1L,
            10L,
            100L,
            1000L,
            10000L,
            100000L,
            1000000L,
            10000000L,
            100000000L
        };

#if !Net4
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
        internal static long Pow10(int power)
        {
            if (power < PowersOf10.Length)
                return PowersOf10[power];
            return (long)Math.Pow(10, power);
        }
    }
}
