using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Serialization;
using log4net;
using log4net.Appender;
using log4net.Config;

namespace Orbiter.Multiplayer.Client
{
    /// <summary>
    /// Managed part of the OMPClient.
    /// </summary>
    public class ManagedPart
    {
        private NTPClient ntpClient=new NTPClient();
        private STUNClient stunClient=new STUNClient();
        private bool blocked;
        private EventThrowAppender appender;
        private LoggerDelegate logger;
        private Control testControl;
        private Database database;
        private History<string> commandHistory;
        private static readonly XmlSerializer ConfigurationSerializer = new XmlSerializer(typeof(OMPClientConfiguration));

        private delegate void LoggerDelegate(int level, IntPtr text);

        public ManagedPart()
        {
            commandHistory=new History<string>(100, "");
            ClientControl control = new ClientControl();
            database=new Database();
            control.SetDatabase(database);
            control.Init(true);
            testControl = control;
        }

        private static readonly ILog rootLog = LogManager.GetLogger("    ");
        private static readonly ILog chatLog = LogManager.GetLogger("Chat");
        private static readonly ILog tcpLog = LogManager.GetLogger(" TCP");
        private static readonly ILog udpLog = LogManager.GetLogger(" UDP");
        private static readonly ILog clockLog = LogManager.GetLogger(" CLK");
        private static readonly ILog gcLog = LogManager.GetLogger("  GC");
        private OMPClientConfiguration configuration;
        private string configurationFileName;

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public void Start()
        {
            //Configuration setup
            var configurationPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..");
            configurationFileName = Path.Combine(configurationPath, "OMPClient.xml");
            using (var file = File.OpenRead(configurationFileName))
                configuration = ConfigurationSerializer.Deserialize(file) as OMPClientConfiguration;
            if (configuration.NTP==null) configuration.NTP=new OMPClientConfigurationNTP(); //Create empty NTP configuration in case of PTP setup
            
            //Logging setup
            XmlDocument logConfiguration=new XmlDocument();
            logConfiguration.Load(Path.Combine(configurationPath, "OMPClient.log4net"));
            XmlConfigurator.Configure(logConfiguration.DocumentElement);
            foreach (IAppender appender in LogManager.GetRepository().GetAppenders())
            {
                if (!appender.Name.Equals("SNTPEventThrowAppender")) continue;
                this.appender=appender as EventThrowAppender;
                if (this.appender!=null) this.appender.Log += sntpLogger;
            }

            //Start NTP system
            ntpClient = new NTPClient(  configuration.NTP.Samples, configuration.NTP.History, configuration.Timing.SNTP,
                                        configuration.Timing.Resync, configuration.NTP.Offset, configuration.NTP.Skew / 1E6,
                                        10);
            
            //Forward NTP settings to unmanaged system
            Marshal.WriteInt32(SampleWaitTime, ntpClient.SampleWaitTime);
            Marshal.WriteInt32(SynchronizationWaitTime, ntpClient.CycleWaitTime);
            
            //Setup NTP server settings and startup client
            if (configuration.NTP.Items!=null) foreach (var server in configuration.NTP.Items)
                ntpClient.AddServer(new NTPServerInfo(server.Address){Delay = server.Delay, Misses = server.Misses, MissesAlarm = server.Alarm});
            ntpClient.Progress += ntpClient_Progress;
            ntpClient.Start();
            try
            {
                var stun = configuration.STUN.Split(':');
                var port = stun.Length > 1 ? Convert.ToInt32(stun[1]) : 3478;
                stunClient.EndPoint = new IPEndPoint(Dns.GetHostAddresses(stun[0])[0], port);
                stun = configuration.STUN2.Split(':');
                port = stun.Length > 1 ? Convert.ToInt32(stun[1]) : 3478;
                stunClient.AlternateEndPoint = new IPEndPoint(Dns.GetHostAddresses(stun[0])[0], port);
            }
            catch{}
            stunClient.Progress += stunClient_Progress;
        }

        void stunClient_Progress(object sender, STUNResponse args)
        {
            var message = args.ProgressMessage;
            var level = -2;
            if (message != null) level = 0;
            else switch (args.NetworkType)
            {
                case STUNNetworkType.Blocked:
                    message = "Your system is not receiving UDP packets at all. Possible causes are restrictive " +
                              "firewalls, routers or ISPs. In addition, an unresponsive STUN server could also be"+
                              " the culprit. Best course of action here is to check these elements for settings " +
                              "outside of default, or - if it is e.g. a corporate firewall - to establish proper" +
                              " port-forwarding to a custom port. Specifying known working STUN servers in the "  +
                              "client configuration file with the \"STUN\" and \"STUN2\" element, respectively, " +
                              "may also yield different results if the default servers ceased to exist.\r\nUse "  +
                              "custom port setting with an arbitrary port that you forwarded.\r\n";
                    break;
                case STUNNetworkType.Cone:
                    message = "Your system is eventually connected to the open internet through a full cone " +
                              "NAT. I.e.: as soon as your machine sent packets out through the NAT, it will " +
                              "get all packets sent to this address and port from everywhere.\r\nDo NOT use " +
                              "custom port setting, just connect. Chances are very good that it'll work flawlessly.\r\n";
                    break;
                case STUNNetworkType.Restricted:
                    message = "Your system is eventually connected to the open internet through a restricted " +
                              "cone NAT. I.e.: as soon as your machine sent packets out to a specific IP through " +
                              "the NAT, it will get all packets sent to this address and port from the IP.\r\n" +
                              "Do NOT use custom port setting at first, just connect. Chances are good that it'll " +
                              "work flawlessly.\r\nIf it doesn't work, use custom port setting with an arbitrary " +
                              "port that you forwarded.\r\n";
                    break;
                case STUNNetworkType.PortRestricted:
                    message = "Your system is eventually connected to the open internet through a port restricted " +
                              "cone NAT. I.e.: as soon as your machine sent packets out to a specific IP AND port " +
                              "through the NAT, it will get all packets sent to this address and port back from the " +
                              "IP AND port.\r\nDo NOT use custom port setting at first, just connect. Chances are " +
                              "that it'll work flawlessly.\r\nIf it doesn't work, use custom port setting with an " +
                              "arbitrary port that you forwarded.\r\n";
                    break;
                case STUNNetworkType.Symmetric:
                    message = "Your system is eventually connected to the open internet through a symmetric NAT. " +
                              "I.e.: Only as long as your machine sent packets out to a specific IP and port through " +
                              "the NAT, it will receive packets from there. If the IP and port is changed, so is the " +
                              "NAT endpoint, breaking OMP session initiation protocol.\r\nUse custom port setting " +
                              "with an arbitrary port that you forwarded.\r\n";
                    break;
                case STUNNetworkType.Open:
                    message = "Your system is not behind a NAT. Do NOT use custom port or hairpinning setting, just " +
                              "connect.\r\n";
                    break;
                case STUNNetworkType.Firewall:
                    message = "Your system is not behind a NAT, but behind a firewall blocking unsolicited UDP packages. " +
                              "You can either add Orbiter as exception to the firewall, turn it off completely, or allow " +
                              "an arbitrary port for incoming traffic. In the later case, use custom port setting with " +
                              "that port. Otherwise, do NOT use custom port setting, just connect.\r\n";
                    break;
                default:
                    message = "The system was unable to determine your internet connection details due to a " +
                              "failure of test I after test II! This is indicating a probable connection break " +
                              "during testing and/or a faulty alternate STUN server. Please consider specifying "+
                              "another public STUN server with the \"STUN2\" element in the client configuration "+
                              "and retry the analysis.\r\n";
                    break;
            }
            IntPtr text = Marshal.StringToHGlobalAnsi(message);
            logger(level, text);
            Marshal.FreeHGlobal(text);
        }

        private void sntpLogger(object sender, EventThrowAppenderEventArgs e)
        {
            IntPtr text = Marshal.StringToHGlobalAnsi(e.Message);
            logger(4, text);
            Marshal.FreeHGlobal(text);
        }

        private void ntpClient_Progress(object sender, NTPClientProgressEventArgs e)
        {
            switch (e.Kind)
            {
                case NTPClientProgressKind.Pinging:
                    if (Marshal.ReadInt32(ClockVisibility)!=0)
                    {
                        Marshal.WriteByte(ClockPulseVisibility, 1);
                        database.ClockPulseVisible = true;
                    }
                    blocked = false;
                    break;
                case NTPClientProgressKind.Blocked:
                    if (Marshal.ReadInt32(ClockVisibility) != 0)
                    {
                        Marshal.WriteInt32(ClockVisibility, 2);
                        database.ClockVisibility = Client.ClockVisibility.Visible;
                    }
                    blocked = true;
                    break;
                case NTPClientProgressKind.Pinged:
                    if (blocked)
                    {
                        Marshal.WriteInt32(ClockVisibility, 1);
                        database.ClockVisibility = Client.ClockVisibility.Fading;
                    }
                    break;
                case NTPClientProgressKind.Failed:
                    Marshal.WriteInt32(ClockVisibility, 0);
                    database.ClockVisibility=Client.ClockVisibility.Hidden;
                    break;
                case NTPClientProgressKind.Idle:
                    database.FreezeStatusData();
                    if (Marshal.ReadInt32(ClockVisibility) == 2)
                    {
                        Marshal.WriteInt32(ClockVisibility, 1);
                        database.ClockVisibility=Client.ClockVisibility.Fading;
                    }
                    if (Marshal.ReadInt32(ClockVisibility) == 0)
                    {
                        Marshal.WriteInt32(ClockVisibility, 2);
                        database.ClockVisibility=Client.ClockVisibility.Visible;
                    }	
                    Marshal.WriteByte(ClockPulseVisibility, 0);
                    database.ClockPulseVisible = false;
                    database.ThawStatusData();
                    break;
                case NTPClientProgressKind.Offline:
                    database.FreezeStatusData();
                    Marshal.WriteInt32(ClockVisibility, 0);
                    database.ClockVisibility=Client.ClockVisibility.Hidden;
                    Marshal.WriteByte(ClockPulseVisibility, 0);
                    database.ClockPulseVisible=false;
                    database.ThawStatusData();
                    break;
                case NTPClientProgressKind.Synced:
                    database.FreezeStatusData();
                    int skew = (int)(ntpClient.SkewMinimum * 1E6);
                    Marshal.WriteInt32(SynchronizationMinimum, skew);
                    database.SkewMin = skew;
                    skew = (int) (ntpClient.SkewMaximum*1E6);
                    Marshal.WriteInt32(SynchronizationMaximum, skew);
                    database.SkewMax = skew;
                    skew = (int)(ntpClient.Skew * 1E6);
                    Marshal.WriteInt32(SynchronizationAverage, skew);
                    database.Skew = skew;
                    database.ThawStatusData();
                    break;
            }
        }
        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            ntpClient.Stop();
            ntpClient.Progress -= ntpClient_Progress;
            stunClient.Progress -= stunClient_Progress;
            if (appender!=null) appender.Log -= sntpLogger;
        }
        /// <summary>
        /// Updates this instance.
        /// </summary>
        public void Update()
        {
            ntpClient.SampleWaitTime = Marshal.ReadInt32(SampleWaitTime);
            ntpClient.CycleWaitTime = Marshal.ReadInt32(SynchronizationWaitTime);
            Marshal.WriteInt32(SampleWaitTime, ntpClient.SampleWaitTime);
            Marshal.WriteInt32(SynchronizationWaitTime, ntpClient.CycleWaitTime);
        }
        /// <summary>
        /// Propagates this instance's data.
        /// </summary>
        public void Propagate()
        {
            database.FreezeStatusData();
            database.Mjd = Marshal.ReadInt32(MjdAverage);
            database.MjdMax = Marshal.ReadInt32(MjdMaximum);
            database.MjdMin = Marshal.ReadInt32(MjdMinimum);
            database.SystemVisible = Marshal.ReadByte(SystemVisibility) != 0;
            database.Pulse = Marshal.ReadInt32(Pulse);
            database.ThawStatusData();
        }
        public IntPtr GetNewWindow()
        {
            testControl.CreateControl();
            return testControl.Handle;
        }
        /// <summary>
        /// Generates the SNTP log.
        /// </summary>
        /// <returns>A <seealso cref="StringBuilder"/> object containing the SNTP log.</returns>
        public StringBuilder GenerateSNTPLog()
        {
            StringBuilder s=new StringBuilder();
            int i = 1;
            foreach (NTPServerInfo server in ntpClient.Servers)
            {
                s.Append('[');
                s.Append(i++);
                if (server.Misses<=server.MissesAlarm*server.Delay)
                {
                    if (server.Suspensions==0)
                    {
                        if (server.Misses<=server.MissesMaximum) s.Append("]   OK since ");
                        else s.Append("] PENDING at ");
                        s.Append(server.Misses);
                        s.Append(" misses");
                    }
                    else
                    {
                        s.Append("] BLOCKED at ");
                        s.Append(server.Misses);
                        s.Append(" misses for ");
                        s.Append(server.Suspensions);
                        s.Append(" hits");
                    }
                }
                else
                {
                    s.Append("] BLOCKED at ");
                    s.Append(server.Misses);
                    s.Append(" misses PERMANENTLY");
                }
                s.Append(": ");
                s.Append(server.Address);
                s.Append("\r\n");
            }
            return s;
        }

        public void Add(string message)
        {
            commandHistory.Add(message.TrimEnd('\n', '\r'));
        }
        public string Up(){return commandHistory.Up();}
        public string Down(){return commandHistory.Down();}
        public string Version(){return new Version().ToString();}
        public string Name
        {
            get { return configuration.Name; }
            set { configuration.Name = value; }
        }
        public string Password
        {
            get { return configuration.Password; }
            set { configuration.Password = value; }
        }
        public string IP
        {
            get { return configuration.Network.IP; }
            set { configuration.Network.IP = value; }
        }
        public int TCP
        {
            get { return configuration.Network.TCP; }
            set { configuration.Network.TCP = value; }
        }
        public int UDP
        {
            get { return configuration.Network.UDP; }
            set { configuration.Network.UDP = value; }
        }
        public int Method
        {
            get { return ((int) configuration.Network.Configuration) - 1; }
            set { configuration.Network.Configuration = (OMPClientConfigurationNetworkConfiguration) value+1; }
        }
        public string Scenario
        {
            get { return configuration.Scenario; }
        }
        public bool Container
        {
            get { return configuration.Container; }
            set { configuration.Container = value; }
        }
        public bool QuickLaunch
        {
            get { return configuration.QuickLaunch; }
            set { configuration.QuickLaunch = value; }
        }
        public bool QuickLeave
        {
            get { return configuration.QuickLeave; }
            set { configuration.QuickLeave = value; }
        }
        public void SaveConfig()
        {
            var skew = (int)(ntpClient.Skew * 1E6);
            configuration.NTP.Offset = ntpClient.Offset;
            configuration.NTP.Skew = skew;
            using (var file = new FileStream(configurationFileName, FileMode.Create))
                ConfigurationSerializer.Serialize(file, configuration);
        }

        /// <summary>
        /// Gets the current compensated time.
        /// </summary>
        /// <value>The current compensated time.</value>
        public double Now
        {
            get
            {
                return ntpClient.Now.ToDouble;
            }
        }
        /// <summary>
        /// Gets the current local time.
        /// </summary>
        /// <value>The current local time.</value>
        public double NowLocal
        {
            get
            {
                return NTPTime.Now.ToDouble;
            }
        }

        /// <summary>
        /// Gets or sets the MJD average.
        /// </summary>
        /// <value>The MJD average.</value>
        public IntPtr MjdAverage { get; set; }

        /// <summary>
        /// Gets or sets the MJD maximum.
        /// </summary>
        /// <value>The MJD maximum.</value>
        public IntPtr MjdMaximum { get; set; }

        /// <summary>
        /// Gets or sets the MJD minimum.
        /// </summary>
        /// <value>The MJD minimum.</value>
        public IntPtr MjdMinimum { get; set; }

        /// <summary>
        /// Gets or sets the synchronization average.
        /// </summary>
        /// <value>The synchronization average.</value>
        public IntPtr SynchronizationAverage { get; set; }

        /// <summary>
        /// Gets or sets the synchronization maximum.
        /// </summary>
        /// <value>The synchronization maximum.</value>
        public IntPtr SynchronizationMaximum { get; set; }

        /// <summary>
        /// Gets or sets the synchronization minimum.
        /// </summary>
        /// <value>The synchronization minimum.</value>
        public IntPtr SynchronizationMinimum { get; set; }

        /// <summary>
        /// Gets or sets the system visibility.
        /// </summary>
        /// <value>The system visibility.</value>
        public IntPtr SystemVisibility { get; set; }

        /// <summary>
        /// Gets or sets the pulse.
        /// </summary>
        /// <value>The pulse.</value>
        public IntPtr Pulse { get; set; }

        /// <summary>
        /// Gets or sets the clock visibility.
        /// </summary>
        /// <value>The clock visibility.</value>
        public IntPtr ClockVisibility { get; set; }

        /// <summary>
        /// Gets or sets the clock pulse visibility.
        /// </summary>
        /// <value>The clock pulse visibility.</value>
        public IntPtr ClockPulseVisibility { get; set; }

        /// <summary>
        /// Gets or sets the sample wait time.
        /// </summary>
        /// <value>The sample wait time.</value>
        public IntPtr SampleWaitTime { get; set; }

        /// <summary>
        /// Gets or sets the synchronization wait time.
        /// </summary>
        /// <value>The synchronization wait time.</value>
        public IntPtr SynchronizationWaitTime { get; set; }

        /// <summary>
        /// Sets the logger delegate.
        /// </summary>
        /// <value>The logger delegate.</value>
        public IntPtr Logger
        {
            set
            {
                logger=(LoggerDelegate) Marshal.GetDelegateForFunctionPointer(value, typeof (LoggerDelegate));
            }
        }

        public void Log(int level, string message)
        {
            switch (level)
            {
                case 10:
                    rootLog.Error(message);
                    break;
                case 1:
                    rootLog.Info(message);
                    break;
                case 20:
                    tcpLog.Error(message);
                    break;
                case 2:
                    tcpLog.Info(message);
                    break;
                case 30:
                    udpLog.Error(message);
                    break;
                case 3:
                    udpLog.Info(message);
                    break;
                case 5:
                    rootLog.Debug(message);
                    break;
                case 6:
                    clockLog.Info(message);
                    break;
                case 7:
                    gcLog.Info(message);
                    break;
                case 9:
                    udpLog.Debug(message);
                    break;
                case 100: //Special tcp log from old EX2 logging
                    tcpLog.Debug(message);
                    break;
                case 101: //Special chat log from old EX2 logging
                    chatLog.Info(message);
                    break;
                case 102: //Special raw input log from old EX logging
                    {
                        int i=0, j = message.IndexOf('\n');
                        while(j>=0)
                        {
                            tcpLog.Debug(message.Substring(i, j-i));
                            i = j + 1;
                            j = message.IndexOf('\n', i);
                        }
                    }
                    break;
            }
        }

        /// <summary>
        /// Analyses STUN situation.
        /// </summary>
        public void Analyse(){stunClient.Request();}

        /// <summary>
        /// Forwards PTP samples to the NTP client.
        /// </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)
        {
            ntpClient.PTPSample(t1,t2,t3,t4);
        }

        /// <summary>
        /// Compares the version strings.
        /// </summary>
        /// <param name="v1">The first version.</param>
        /// <param name="v2">The second version.</param>
        /// <returns>
        /// -1 if the first version is greater than the second,
        ///  0 if equal,
        ///  1 if the first version is less than the second,
        /// </returns>
        public int CompareVersions(string v1, string v2)
        {
            var a = Multiplayer.Version.Parse(v1);
            var b = Multiplayer.Version.Parse(v2);
            return a<b?1:(a>b?-1:0);
        }
    }
}
