using System;

namespace Orbiter.Multiplayer
{
    /// <summary>
    /// NTP time class.
    /// </summary>
    public class NTPTime
    {
        private const double fractionToSecond = (((double)1.0) / 0xFFFFFFFF);
        private const long Date1900_0101 = 2415021;

        private ulong time;

        /// <summary>
        /// Initializes a new instance of the <see cref="NTPTime"/> class.
        /// </summary>
        public NTPTime(){time = 0;}
        /// <summary>
        /// Initializes a new instance of the <see cref="NTPTime"/> class.
        /// </summary>
        /// <param name="time">The time.</param>
        public NTPTime(NTPTime time){this.time = time.time;}
        /// <summary>
        /// Initializes a new instance of the <see cref="NTPTime"/> class.
        /// </summary>
        /// <param name="packet">The packet.</param>
        public NTPTime(NTPTimePacket packet)
        {
            HostVsNetwork convert=new HostVsNetwork();
            convert.HostUInt32 = packet.Integer;
            convert.HostToNetwork(4);
            time = (ulong)convert.NetworkUInt32 << 32;
            convert.HostUInt32 = packet.Fraction;
            convert.HostToNetwork(4);
            time += convert.NetworkUInt32;
        }

        /// <summary>
        /// Gets the double representation.
        /// </summary>
        /// <value>The double representation.</value>
        public double ToDouble
        {
            get
            {
                return Seconds + FractionToSecond(Fraction);
            }
        }
        
        /// <summary>
        /// Creates an NTPTime based on a HighResolutionDateTime value.
        /// </summary>
        /// <param name="st">The HighResolutionDateTime value to be used.</param>
        /// <remarks>
        /// <note>Only the primary epoch defined by NTP is supported (1900-2036)!</note>
        /// </remarks>
        public NTPTime(HighResolutionDateTime st)
        {
            //Currently this function only operates correctly in 
            //the 1900 - 2036 primary epoch defined by NTP

            long JD = GetJulianDay(st.Year, st.Month, st.Day);
            JD -= Date1900_0101;

            ulong seconds = (ulong)JD;
            seconds = (seconds * 24) + (ulong)st.Hour;
            seconds = (seconds * 60) + (ulong)st.Minute;
            seconds = (seconds * 60) + (ulong)st.Second;
            time = (seconds << 32) + SecondsToFraction(st.Fraction);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NTPTime"/> class.
        /// </summary>
        /// <param name="seconds">The seconds.</param>
        /// <param name="fraction">The fraction.</param>
        public NTPTime(double seconds, double fraction)
        {
            time = (((ulong) seconds) << 32) + SecondsToFraction(fraction);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NTPTime"/> class.
        /// </summary>
        /// <param name="seconds">The seconds including fractional part.</param>
        public NTPTime(double seconds)
        {
            time = (((ulong)seconds) << 32) + SecondsToFraction(seconds - Math.Truncate(seconds));
        }

        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="time1">The time1.</param>
        /// <param name="time2">The time2.</param>
        /// <returns>The result of the operator.</returns>
        public static double operator -(NTPTime time1, NTPTime time2)
        {
            ulong delta;
            if (time1>time2)
            {
                delta = time1.time - time2.time;
                return (delta >> 32) + FractionToSecond((uint)(delta & 0xFFFFFFFF));
            }
            else
            {
                delta = time2.time - time1.time;
                return -((delta >> 32) + FractionToSecond((uint)(delta & 0xFFFFFFFF)));
            }
        }
        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="time1">The time1.</param>
        /// <param name="timeSpan">The time span.</param>
        /// <returns>The result of the operator.</returns>
        public static NTPTime operator +(NTPTime time1, double timeSpan)
        {
            double time1Value = time1.ToDouble+timeSpan;
            double seconds = Math.Truncate(time1Value);
            return new NTPTime(seconds, time1Value - seconds);
        }
        /// <summary>
        /// Performs an implicit conversion from <see cref="Orbiter.Multiplayer.NTPTime"/> to <see cref="Orbiter.Multiplayer.NTPTimePacket"/>.
        /// </summary>
        /// <param name="time">The time.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator NTPTimePacket(NTPTime time)
        {
            NTPTimePacket pack=new NTPTimePacket();
            HostVsNetwork convert=new HostVsNetwork();
            convert.HostUInt32 = time.Seconds;
            convert.HostToNetwork(4);
            pack.Integer = convert.NetworkUInt32;
            convert.HostUInt32 = time.Fraction;
            convert.HostToNetwork(4);
            pack.Fraction = convert.NetworkUInt32;
            return pack;
        }
        /// <summary>
        /// Performs an implicit conversion from <see cref="Orbiter.Multiplayer.NTPTime"/> to <see cref="System.UInt64"/>.
        /// </summary>
        /// <param name="time">The time.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator ulong(NTPTime time) { return time.time; }
        /// <summary>
        /// Performs an implicit conversion from <see cref="Orbiter.Multiplayer.NTPTime"/> to <see cref="Orbiter.Multiplayer.HighResolutionDateTime"/>.
        /// </summary>
        /// <param name="time">The time.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator HighResolutionDateTime(NTPTime time)
        {
            //Currently this function only operates correctly in 
            //the 1900 - 2036 primary epoch defined by NTP

            uint s = time.Seconds;
            int second = (int)(s % 60);
            s /= 60;
            int minute = (int)(s % 60);
            s /= 60;
            int hour = (int)(s % 24);
            s /= 24;
            long JD = s + Date1900_0101;
            DateTime gregorianDate = GetGregorianDate(JD);
            return new HighResolutionDateTime(gregorianDate.Year, gregorianDate.Month, gregorianDate.Day, hour, minute, second, FractionToSecond(time.Fraction), DateTimeKind.Utc);
        }
        /// <summary>
        /// Gets the seconds.
        /// </summary>
        /// <value>The seconds.</value>
        public uint Seconds{get { return (uint)(time >> 32); }}
        /// <summary>
        /// Gets the fraction.
        /// </summary>
        /// <value>The fraction.</value>
        public uint Fraction{get { return (uint)(time & 0xFFFFFFFF); }}

        /// <summary>
        /// Gets the current time.
        /// </summary>
        /// <value>The current time.</value>
        public static NTPTime Now{get{return new NTPTime(HighResolutionDateTime.Now);}}
        /// <summary>
        /// Fractions to second.
        /// </summary>
        /// <param name="dwFraction">The fraction.</param>
        /// <returns>The seconds.</returns>
        public static double FractionToSecond(UInt32 dwFraction){return dwFraction * fractionToSecond;}
        /// <summary>
        /// Seconds to fraction.
        /// </summary>
        /// <param name="seconds">The seconds.</param>
        /// <returns>The fraction.</returns>
        public static UInt32 SecondsToFraction(double seconds) { return (UInt32)(seconds * 0xFFFFFFFF); }

        /// <summary>
        /// Gets the julian day.
        /// </summary>
        /// <param name="Year">The year.</param>
        /// <param name="Month">The month.</param>
        /// <param name="Day">The day.</param>
        /// <returns>The JD.</returns>
        public static Int64 GetJulianDay(int Year, int Month, int Day)
        {
            long y = Year;
            long m = Month;
            if (m > 2)
            {
                m = m - 3;
            }
            else
            {
                m = m + 9;
                y = y - 1;
            }
            long c = y / 100;
            return (146097L * c) / 4 + (1461L * (y - 100 * c)) / 4 + (153L * m + 2) / 5 + Day + 1721119L;
        }
        /// <summary>
        /// Gets the gregorian date.
        /// </summary>
        /// <param name="JD">The JD.</param>
        /// <returns>The gregorian date/time.</returns>
        public static DateTime GetGregorianDate(long JD)
        {
            long j = JD - 1721119;
            long y = (4 * j - 1) / 146097;
            j = 4 * j - 1 - 146097 * y;
            long d = j / 4;
            j = (4 * d + 3) / 1461;
            d = 4 * d + 3 - 1461 * j;
            d = (d + 4) / 4;
            long m = (5 * d - 3) / 153;
            d = 5 * d - 3 - 153 * m;
            d = (d + 5) / 5;
            y = 100 * y + j;
            if (m < 10)
            {
                m = m + 3;
            }
            else
            {
                m = m - 9;
                y = y + 1;
            }

            return new DateTime((int)y, (int)m, (int)d);
        }
    }
}
