using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using log4net;

namespace Orbiter.Multiplayer
{
    /// <summary>
    /// SNTP client for retrieving time information from NTP servers.
    /// </summary>
    public class NTPClient
    {
        private NTPTime baseTime;
        private NTPTime cycleBase;
        private double offsetAdjustment;
        private readonly List<NTPServerInfo> servers=new List<NTPServerInfo>();
        private Thread synchronizerThread;
        private bool synchronizing;
        private readonly int sntpSamples;
        private readonly int sntpHistory;
        private int sampleWaitFirstHalfTime;
        private int sampleWaitSecondHalfTime;
        private int cycleWaitFirstTimePart;
        private int cycleWaitSecondTimePart;
        private static readonly ILog Log = LogManager.GetLogger("SNTP");
        private readonly ManualResetEvent added = new ManualResetEvent(true);
        private double[] samples;
        private double[] cycles;
        private int currentSample;
        private int currentHistory;
        private int history;
        private double samplesAverage;
        private readonly Random random = new Random();
        private bool initialized;
        private bool ptpPingToggle;

        /// <summary>
        /// Gets the offset.
        /// </summary>
        /// <value>The offset.</value>
        public double Offset { get; private set; }

        /// <summary>
        /// Gets the skew.
        /// </summary>
        /// <value>The skew.</value>
        public double Skew { get; private set; }

        /// <summary>
        /// Gets the skew minimum.
        /// </summary>
        /// <value>The skew minimum.</value>
        public double SkewMinimum { get; private set; }

        /// <summary>
        /// Gets the skew maximum.
        /// </summary>
        /// <value>The skew maximum.</value>
        public double SkewMaximum { get; private set; }

        /// <summary>
        /// Adds a server.
        /// </summary>
        /// <param name="server">The server.</param>
        public void AddServer(NTPServerInfo server)
        {
            servers.Add(server);
            added.Set();
        }

        /// <summary>
        /// Removes a server.
        /// </summary>
        /// <param name="server">The server.</param>
        public void RemoveServer(NTPServerInfo server)
        {
            servers.Remove(server);
        }

        /// <summary>
        /// Gets the servers.
        /// </summary>
        /// <value>The servers.</value>
        public List<NTPServerInfo> Servers
        {
            get { return servers; }
        }

        /// <summary>
        /// Gets the server time.
        /// </summary>
        /// <param name="hostName">Name of the host.</param>
        /// <returns></returns>
        public NTPServerResponse GetServerTime(string hostName)
        {
            return GetServerTime(hostName, 123);
        }

        /// <summary>
        /// Gets the server time.
        /// </summary>
        /// <param name="hostName">Name of the host.</param>
        /// <param name="port">The port.</param>
        /// <returns></returns>
        public NTPServerResponse GetServerTime(string hostName, int port)
        {
            //Connect socket
            var pSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
            {
                ReceiveTimeout = 2000,
                SendTimeout = 2000
            };
            pSocket.Connect(new IPEndPoint(Dns.GetHostAddresses(hostName)[0], port));

            //Initialise the NTPBasicInfo packet
            var nbi = new NTPBasicInfo {VersionMode = 27, TransmitTimestamp = Now};
            if (baseTime == null) baseTime = NTPTime.Now;

            //Send NTPBasicInfoPacket
            var rawdatas = new byte[Marshal.SizeOf(nbi)];
            var buffer = Marshal.AllocHGlobal(rawdatas.Length);
            Marshal.StructureToPtr(nbi, buffer, false);
            Marshal.Copy(buffer, rawdatas, 0, rawdatas.Length);
            Marshal.FreeHGlobal(buffer);
            pSocket.Send(rawdatas);

            //Read server response as NTPFullPacket
            rawdatas = new byte[Marshal.SizeOf(new NTPFullPacket())];
            pSocket.Receive(rawdatas);
            buffer = Marshal.AllocHGlobal(rawdatas.Length);
            Marshal.Copy(rawdatas, 0, buffer, rawdatas.Length);
            var nfp = (NTPFullPacket)Marshal.PtrToStructure(buffer, typeof(NTPFullPacket));
            Marshal.FreeHGlobal(buffer);

            //Disconnect from server
            pSocket.Close();

            //Create response
            var originate = new NTPTime(nfp.Basic.OriginateTimestamp);
            var receive = new NTPTime(nfp.Basic.ReceiveTimestamp);
            var transmit = new NTPTime(nfp.Basic.TransmitTimestamp);
            var destination = Now;
            return new NTPServerResponse(
                (nfp.Basic.VersionMode & 0xC0) >> 6,
                nfp.Basic.Stratum,
                new [] { originate, receive, transmit, destination },
                ((destination - originate) - (receive - transmit)),
                ((receive - originate) + (transmit - destination)) / 2);
        }

        /// <summary>
        /// Gets the current compensated time.
        /// </summary>
        /// <value>The current compensated time.</value>
        public NTPTime Now
        {
            get
            {
                lock (this)
                {
                    var t = NTPTime.Now;
                    var d = t - baseTime;
                    var dA = Math.Max(d, CycleWaitTime);
                    t = t + Offset + d*Skew + dA*offsetAdjustment;
                    return t;
                }
            }
        }

        /// <summary>
        /// Gets or sets the sample wait time.
        /// </summary>
        /// <value>The sample wait time.</value>
        public int SampleWaitTime
        {
            get
            {
                return sampleWaitFirstHalfTime + sampleWaitSecondHalfTime;
            }
            set
            {
                var oldCycle = CycleWaitTime;
                sampleWaitFirstHalfTime = value / 2;
                if (sampleWaitFirstHalfTime < 1)
                {
                    sampleWaitFirstHalfTime = 1;
                    sampleWaitSecondHalfTime = 1;
                }
                else
                {
                    sampleWaitSecondHalfTime = value - sampleWaitFirstHalfTime;
                }
                CycleWaitTime = oldCycle;
            }
        }
        /// <summary>
        /// Gets or sets the cycle wait time.
        /// </summary>
        /// <value>The cycle wait time.</value>
        public int CycleWaitTime
        {
            get
            {
                return cycleWaitFirstTimePart + cycleWaitSecondTimePart;
            }
            set
            {
                cycleWaitFirstTimePart = sampleWaitFirstHalfTime;
                cycleWaitSecondTimePart = value - cycleWaitFirstTimePart;
                if (cycleWaitSecondTimePart < 1) cycleWaitSecondTimePart = 1;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NTPClient"/> class.
        /// </summary>
        /// <param name="sntpSamples">The SNTP samples.</param>
        /// <param name="sntpHistory">The SNTP history.</param>
        /// <param name="sampleWaitTime">The sample wait time.</param>
        /// <param name="cycleWaitTime">The cycle wait time.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="skew">The skew.</param>
        /// <param name="accuracy">The accuracy.</param>
        public NTPClient(int sntpSamples, int sntpHistory, int sampleWaitTime, int cycleWaitTime, double offset, double skew, int accuracy)
        {
            this.sntpHistory = sntpHistory < 2 ? 2 : sntpHistory;
            this.sntpSamples = sntpSamples < 1 ? 1 : sntpSamples;
            SampleWaitTime = sampleWaitTime;
            CycleWaitTime = cycleWaitTime;
            Offset = offset;
            Skew = skew;
            SkewMinimum = skew;
            SkewMaximum = skew;
            HighResolutionDateTime.Initialize(accuracy);
            baseTime = NTPTime.Now;
            offsetAdjustment = 0;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NTPClient"/> class.
        /// </summary>
        /// <param name="sntpSamples">The SNTP samples.</param>
        /// <param name="sntpHistory">The SNTP history.</param>
        /// <param name="sampleWaitTime">The sample wait time.</param>
        /// <param name="cycleWaitTime">The cycle wait time.</param>
        public NTPClient(int sntpSamples, int sntpHistory, int sampleWaitTime, int cycleWaitTime):this(sntpSamples, sntpHistory, sampleWaitTime, cycleWaitTime, 0, 0, 10){}
        /// <summary>
        /// Initializes a new instance of the <see cref="NTPClient"/> class.
        /// </summary>
        /// <param name="sampleWaitTime">The sample wait time.</param>
        /// <param name="cycleWaitTime">The cycle wait time.</param>
        public NTPClient(int sampleWaitTime, int cycleWaitTime):this(15,30,sampleWaitTime,cycleWaitTime){}
        /// <summary>
        /// Initializes a new instance of the <see cref="NTPClient"/> class.
        /// </summary>
        public NTPClient():this(2000, 60000){}

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public void Start()
        {
            lock (this)
            {
                synchronizing = true;
                synchronizerThread = new Thread(Synchronizer)
                {
                    IsBackground = true,
                    Name = "NTPSynchronizer",
                    Priority = ThreadPriority.AboveNormal
                };
                synchronizerThread.Start();
            }
        }

        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            if (synchronizerThread != Thread.CurrentThread)
                lock (this)
                {
                    added.Set();
                    synchronizing = false;
                    if (synchronizerThread != null)
                    {
                        if (synchronizerThread.ThreadState != ThreadState.Stopped)
                        {
                            synchronizerThread.Abort();
                            synchronizerThread.Join();
                            synchronizerThread = null;
                        }
                    }
                }
        }

        /// <summary>
        /// Occurs together with NTP synchronization-progress].
        /// </summary>
        public event NTPClientProgressDelegate Progress;

        private void OnProgress(NTPServerInfo server, NTPClientProgressKind kind)
        {
            if (Progress != null)
                Progress(server ?? (object) this, new NTPClientProgressEventArgs(kind));
        }

        private void Synchronizer()
        {
            try
            {
                samples=new double[sntpSamples];
                cycles=new double[sntpHistory];
// ReSharper disable CompareOfFloatsByEqualityOperator
                if (Skew!=0)
// ReSharper restore CompareOfFloatsByEqualityOperator
                {
                    history = sntpHistory;
                    for (var i = 0; i < history; i++) cycles[i] = Skew;
                }
                
                OnProgress(null, NTPClientProgressKind.Synced);
                
                while (synchronizing)
                {
                    if (servers.Count == 0)
                    {
                        added.Reset();
                        added.WaitOne();
                    }
                    NTPServerInfo server = null;
                    try
                    {
                        server=servers[random.Next(0,servers.Count)];
                        OnProgress(server, NTPClientProgressKind.Pinging);
                        server = CheckServerSuspension(server);
                    }
                    catch (ArgumentOutOfRangeException){}
                    if (server!=null)
                    {
                        try
                        {
                            var response = GetServerTime(server.Address);
                            OnProgress(server, NTPClientProgressKind.Pinged);
                            if (!initialized && currentSample <= 0) cycleBase = NTPTime.Now; //In case of mixed NTP/PTP sampling, set the first stamp here, too.
                            ProcessNTPSample(server, response);
                        }
                        catch(Exception ex)
                        {
                            OnProgress(server, NTPClientProgressKind.Failed);
                            Log.Error("Failed to retreive time from server "+server.Address, ex);
                            if (server.Misses<server.MissesMaximum)
                            {
                                server.Misses++;
                                Log.ErrorFormat("{0} fails left for this server...", server.MissesMaximum - server.Misses);					
                            }
                            else
                            {					
                                server.Suspensions=server.Delay*++server.Misses;
                                Log.ErrorFormat("This server is blocked for {0} hits...", server.Suspensions);
                            }
                        }
                    }
                    else
                    {
                        OnProgress(null, NTPClientProgressKind.Offline);
                        Log.Error("No more unblocked server available!");
                    }		

                    if (currentSample>=sntpSamples)
                    {
                        AdjustClock();
                        Thread.Sleep(cycleWaitFirstTimePart);
                        OnProgress(null, NTPClientProgressKind.Idle);
                        OnProgress(null, NTPClientProgressKind.Synced);
                        Thread.Sleep(cycleWaitSecondTimePart);
                    }
                    else
                    {
                        Thread.Sleep(sampleWaitFirstHalfTime);
                        OnProgress(null, NTPClientProgressKind.Idle);
                        Thread.Sleep(sampleWaitSecondHalfTime);
                    }
                }
                synchronizerThread = null;
            }
            catch (ThreadAbortException) { }
        }

        private void AdjustClock()
        {
            //Calculate history average
            var time1 = NTPTime.Now;
            var timeSpan = time1 - baseTime;

            if (initialized)
            {
                double cyclesAverage;
                double cyclesMinimum;
                double cyclesMaximum;
                cycles[currentHistory] = Skew + samplesAverage/timeSpan;
                if (history > 1)
                {
                    cyclesMinimum = cycles[0];
                    cyclesMaximum = cycles[0];
                    cyclesAverage = cycles[0];
                    for (var i = 1; i < history; i++)
                    {
                        if (cycles[i] < cyclesMinimum) cyclesMinimum = cycles[i];
                        if (cycles[i] > cyclesMaximum) cyclesMaximum = cycles[i];
                        cyclesAverage += cycles[i];
                    }
                    cyclesAverage /= history + 1;
                }
                else if (history > 0)
                {
                    cyclesAverage = (cycles[0] + cycles[1])/2;
                    if (cycles[0] > cycles[1])
                    {
                        cyclesMinimum = cycles[0];
                        cyclesMaximum = cycles[1];
                    }
                    else
                    {
                        cyclesMinimum = cycles[1];
                        cyclesMaximum = cycles[0];
                    }
                }
                else
                {
                    cyclesAverage = cyclesMinimum = cyclesMaximum = cycles[0];
                }

                lock (this)
                {
                    Offset += CycleWaitTime*offsetAdjustment;
                    offsetAdjustment = (samplesAverage + timeSpan*Skew)/CycleWaitTime;
                    baseTime = time1;
                    Skew = cyclesAverage;
                    SkewMinimum = cyclesMinimum;
                    SkewMaximum = cyclesMaximum;
                }

                Log.InfoFormat(
                    "Skew:{0}us/s (min.{2} / max.{1}) Used:{3}us/s  Adjustment:{4}ns/s",
                    (int) (cycles[currentHistory]*1E6),
                    (int) (cyclesMaximum*1E6),
                    (int) (cyclesMinimum*1E6),
                    (int) (cyclesAverage*1E6),
                    (int) (offsetAdjustment*1E9)
                    );

                if (++currentHistory >= sntpHistory) currentHistory = 0;
                if (history < sntpHistory) history++;
            }
            else
            {
                lock (this)
                {
                    offsetAdjustment = 0;
                    Offset += samplesAverage + timeSpan*Skew;
                    baseTime = time1;
                }
                Log.Info("Startup SNTP period finished.");
            }
            initialized = true;
            currentSample = 0;
        }

        private NTPServerInfo CheckServerSuspension(NTPServerInfo server)
        {
            if (server.Suspensions > 0)
            {
                if (!(server.Misses > server.MissesAlarm*server.Delay))
                {
                    OnProgress(server, NTPClientProgressKind.Blocked);
                    server.Suspensions--;
                    Log.InfoFormat("Server {0} was hit, but blocked... {1} hits left.", server.Address, server.Suspensions);
                }
                var first = server;
                //Random direction for linear search
                var hours = (long) random.Next(0, 2)*-2 + 1;
                var index = servers.IndexOf(server) + (int) hours;
                if (index >= servers.Count) index = 0;
                if (index < 0) index = servers.Count - 1;
                Log.InfoFormat("Searching {0}.", hours > 0 ? "upwards" : "downwards");
                while ((server = servers[index]).Suspensions > 0 && server != first)
                {
                    index += (int) hours;
                    if (index >= servers.Count) index = 0;
                    if (index < 0) index = servers.Count - 1;
                }
                if (server == first) server = null;
            }
            return server;
        }

        private void ProcessNTPSample(NTPServerInfo server, NTPServerResponse response)
        {
            server.Misses = 0;
            samples[currentSample] = response.LocalClockOffset;

            //Calculate precision workout
            if (currentSample > 1)
            {
                //Calculate FTA median
                samplesAverage = 0;
                var local = new List<double>();
                for (var i = 0; i < currentSample + 1; i++) local.Add(samples[i]);
                local.Sort();
                samplesAverage = local[local.Count/2];
            }
            else if (currentSample > 0) samplesAverage = (samples[0] + samples[1])/2;
            else samplesAverage = samples[0];

            Log.Info(String.Format("Offset:{0,6:F3}s  Trip:{1,6:F3}s  Avg.:{3,7:F4}s [{4}. {2}]", response.LocalClockOffset,
                                   response.RoundTripDelay, server.Address, samplesAverage, currentSample));

            currentSample++;
        }

        /// <summary>
        /// Informs the NTPClient of external PTP sample.
        /// </summary>
        /// <param name="t1">The PTP server sent time.</param>
        /// <param name="t2">The PTP client receive time.</param>
        /// <param name="t3">The PTP client request time.</param>
        /// <param name="t4">The PTP server request time.</param>
        public void PTPSample(double t1, double t2, double t3, double t4)
        {
            if (!initialized && currentSample <= 0) cycleBase = NTPTime.Now; //First stamp to measure cycle base time.
            ProcessNTPSample(new NTPServerInfo("PTP"), new NTPServerResponse( 0, 0, new[]
                             {
                                 new NTPTime(t1), new NTPTime(t2), new NTPTime(t3), new NTPTime(t4)
                             }, (t2-t1)+(t4-t3),((t4-t3)-(t2-t1))/2));
            if (currentSample >= sntpSamples)
            {
                CycleWaitTime = (int) (NTPTime.Now - cycleBase); //Finishing PTP samples will reset the cycle wait time to the measured sampling period only
                AdjustClock();
                ptpPingToggle = false;
                OnProgress(null, NTPClientProgressKind.Idle);
                OnProgress(null, NTPClientProgressKind.Synced);
                cycleBase = NTPTime.Now;
            }
            else
            {
                ptpPingToggle = !ptpPingToggle;
                OnProgress(null, ptpPingToggle?NTPClientProgressKind.Pinging:NTPClientProgressKind.Idle);
            }
        }
    }
}
