using System;
using Orbiter.Platform;

namespace Orbiter.Multiplayer
{
    /// <summary>
    /// DateTime class with high resolution.
    /// </summary>
    public class HighResolutionDateTime
    {
    	private static DateTime origin;
        private static long counter;
        private static long frequency;
        private DateTime dateTime;
        private double fraction;

        /// <summary>
        /// Gets the date time.
        /// </summary>
        /// <value>The date time.</value>
        public DateTime DateTime{get{return dateTime;}}

        /// <summary>
        /// Initializes a new instance of the <see cref="HighResolutionDateTime"/> class.
        /// </summary>
        /// <param name="offset">The offset.</param>
        public HighResolutionDateTime(double offset)
        {
            double fullSeconds = Math.Truncate(offset);
            dateTime=origin.AddSeconds(fullSeconds);
            fraction = offset - fullSeconds;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HighResolutionDateTime"/> class.
        /// </summary>
        /// <param name="year">The year.</param>
        /// <param name="month">The month.</param>
        /// <param name="day">The day.</param>
        /// <param name="hour">The hour.</param>
        /// <param name="minute">The minute.</param>
        /// <param name="second">The second.</param>
        /// <param name="fraction">The fraction.</param>
        /// <param name="kind">The kind.</param>
        public HighResolutionDateTime(int year, int month, int day, int hour, int minute, int second, double fraction, DateTimeKind kind)
        {
            int millisecond =(int)Math.Truncate(fraction*1000);
            dateTime=new DateTime(year, month, day, hour, minute, second, millisecond, kind);
            this.fraction = fraction;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HighResolutionDateTime"/> class.
        /// </summary>
        /// <param name="dateTime">The date time.</param>
        public HighResolutionDateTime(DateTime dateTime)
        {
            this.dateTime = dateTime;
            fraction = 0;
        }

        /// <summary>
        /// Initializes the specified accuracy.
        /// </summary>
        /// <param name="accuracy">The accuracy.</param>
        public static void Initialize(int accuracy)
        {
	        Functions.QueryPerformanceFrequency(out frequency);
            long ac=(long)(accuracy/1E6*frequency);
            DateTime ft0=DateTime.UtcNow;
            DateTime ft1;
            
            do
            {
                ft1=DateTime.UtcNow;
            }
            while (ft0==ft1);
            ulong ftd = (ulong)((ft1 - ft0).Ticks * 1.3);

            // Spin waiting for a change in system time. Get the matching
            // performance counter value for that time.
            long p0;
            long p1;
            long prev_diff;
            do
            {
                prev_diff=0;
                Functions.QueryPerformanceCounter(out p0);
                ft0=DateTime.UtcNow;
                ft1=DateTime.UtcNow;
                Functions.QueryPerformanceCounter(out p1);
                while (ft0==ft1)
                {
                    prev_diff=p1-p0;
                    p0=p1;
                    ft1=DateTime.UtcNow;
                    Functions.QueryPerformanceCounter(out p1);
                }
            }
            while ((ulong)(ft1.Ticks-ft0.Ticks)>ftd && p1-p0+prev_diff<ac);

            origin = ft1;
            ac=(p1-p0+prev_diff)/2;
            p1=p1-ac;
            counter = p1;
        }

        /// <summary>
        /// Gets the current high-resolution date/time.
        /// </summary>
        /// <value>The current date/time.</value>
        public static HighResolutionDateTime Now
        {
            get
            {
                long li;
                Functions.QueryPerformanceCounter(out li);
                return new HighResolutionDateTime((double)(li - counter) / (double)frequency);
            }
        }

        /// <summary>
        /// Gets the year.
        /// </summary>
        /// <value>The year.</value>
        public int Year{get{return dateTime.Year;}}
        /// <summary>
        /// Gets the month.
        /// </summary>
        /// <value>The month.</value>
        public int Month { get { return dateTime.Month; } }
        /// <summary>
        /// Gets the day.
        /// </summary>
        /// <value>The day.</value>
        public int Day { get { return dateTime.Day; } }
        /// <summary>
        /// Gets the hour.
        /// </summary>
        /// <value>The hour.</value>
        public int Hour { get { return dateTime.Hour; } }
        /// <summary>
        /// Gets the minute.
        /// </summary>
        /// <value>The minute.</value>
        public int Minute { get { return dateTime.Minute; } }
        /// <summary>
        /// Gets the second.
        /// </summary>
        /// <value>The second.</value>
        public int Second { get { return dateTime.Second; } }
        /// <summary>
        /// Gets the millisecond.
        /// </summary>
        /// <value>The millisecond.</value>
        public int Millisecond { get { return (int)(fraction*1000); } }

        /// <summary>
        /// Gets the fraction.
        /// </summary>
        /// <value>The fraction.</value>
        public double Fraction{get{return fraction;}}

        /// <summary>
        /// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
        /// </returns>
        public override string ToString()
        {
            return dateTime.ToString();
        }

        private const int OCT5_1582 = 2299160;		// "really" 15-Oct-1582
        private const int OCT14_1582 = 2299169;		// "really"  4-Oct-1582
        private const int JAN1_1 = 1721423;

        private const int YEAR = 365;
        private const int FOUR_YEARS = 1461;
        private const int CENTURY = 36524;
        private const int FOUR_CENTURIES = 146097;

        private static int[][] daysByMonth = 
			        {
			        new int[]{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
			        new int[]{0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}
			        };
        
        //private static string[] DayOfWeek =
        //            { "Sun", "Mon", "Tue", "Wed", "Thu", "Fry", "Sat" };
        //private static string[] MonthOfYear =
        //            { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

        /// <summary>
        /// Returns a date/time from the specified modified julian day.
        /// </summary>
        /// <param name="MJD">The MJD.</param>
        /// <returns></returns>
        public static HighResolutionDateTime FromModifiedJulianDay(double MJD)
        {
	        //Day starts at midnight
	        MJD+=2400000;

	        //Split days from fraction
	        int julian=(int) MJD;
	        double fraction=MJD-julian;

	        //Calculate date
            int	z;
            int	y;
	        short 	m,d;
	        int 	lp;

	        z = julian+1;
	        if (z >= OCT5_1582)
	        {
		        z -= JAN1_1;
		        z  = z + (z/CENTURY)  - (z/FOUR_CENTURIES) -2;
		        z += JAN1_1;
	        }
	        z = z - ((z-YEAR) / FOUR_YEARS);		// Remove leap years before current year
	        y = z / YEAR;
	        d = (short) (z - (y * YEAR));
	        y = y - 4712;							// our base year in 4713BC
	        if (y < 1) y--;
	        lp = (y & 3) == 0 ? 1 : 0;  			// lp = 1 if this is a leap year.
	        if (d==0)
	        {
		        y--;
		        d = (short) (YEAR + lp);
	        }
	        m  = (short) (d/30);					// guess at month
	        while (daysByMonth[lp][m] >=d) m--;		// correct guess
	        
	        //Calculate time
	        ushort hour=(ushort)(fraction*=24);
	        fraction-=hour;
	        ushort minute=(ushort)(fraction*=60);
	        fraction-=minute;
	        ushort second=(ushort)(fraction*=60);
	        fraction-=second;
        	
            return new HighResolutionDateTime(y, m+1, d - daysByMonth[lp][m], hour, minute, second, fraction, DateTimeKind.Utc);
        }

        /// <summary>
        /// Returns the modified julian day.
        /// </summary>
        /// <returns>The MJD.</returns>
        public double ToModifiedJulianDay()
        {
            int a;
            int work_year = Year;
            long j;
            int lp;
            double julian;

            if (work_year < 0) work_year++;				// correct for negative year  (-1 = 1BC = year 0)
            lp = (work_year & 3) == 0 ? 1 : 0;			// lp = 1 if this is a leap year.
            j = ((work_year - 1) / 4) +	// ALL leap years
                daysByMonth[lp][Month-1] +	// days in this year
                Day +	// day in this month
                (work_year * 365L) +	// days in years
                 JAN1_1 +	// first january in year 1
                 -366;									// adjustments
            // deal with Gregorian calendar
            if (j >= OCT14_1582)
            {
                a = work_year / 100;
                j = j + 2 - a + a / 4;						// skip days which didn't exist.
            }
            julian = j - 2400000;							// convert to modified julian days
            return julian +	// return julian days + time fraction
                (((Millisecond / 1000.0 + Second) / 60.0 + Minute) / 60.0 + Hour) / 24.0;
        }
    }
}
