using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Xml.Serialization;
using log4net;

namespace Orbiter.Multiplayer
{
    /// <summary>
    /// The main server class.
    /// </summary>
    public class Server
    {
        private string configurationFileName;
        private OMPServerConfiguration configuration;
        private NTPClient ntpClient;
        private Listener listener;
        private Web web;
        private Map map;
        private Time time;
        private static readonly XmlSerializer ConfigurationSerializer = new XmlSerializer(typeof(OMPServerConfiguration));
        private FileSystemWatcher configurationFileWatcher = new FileSystemWatcher();
        private static readonly ILog log = LogManager.GetLogger("    ");
        private NTPTime lastConfigChange;
        private int configChangeAttempt;
        private ManualResetEvent ntpClientSampled=new ManualResetEvent(false);

        public OMPServerConfiguration Configuration
        {
            get { return configuration; }
        }

        public Map Map
        {
            get { return map; }
        }
        
        public NTPClient NTP
        {
            get { return ntpClient; }
        }

        public Server(string[] args)
        {
            lastConfigChange=new NTPTime();
            configChangeAttempt = 0;
            configurationFileName = "server.xml";
            if (args.Length > 0) configurationFileName = args[0];
        }

        public Listener Listener
        {
            get { return listener; }
        }

        public void Start()
        {
            //Read configuration file
            if (!Path.IsPathRooted(configurationFileName)) configurationFileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configurationFileName);
            if (!File.Exists(configurationFileName))
                throw new FileNotFoundException("Configuration file '" + configurationFileName + "' not found!");
            using (var file=File.OpenRead(configurationFileName))
                configuration = ConfigurationSerializer.Deserialize(file) as OMPServerConfiguration;
            if (configuration.NTP == null) configuration.NTP = new OMPServerConfigurationNTP();
            if (configuration.HTML == null) configuration.HTML = new OMPServerConfigurationHTML();
            
            //Start NTP system
            var sntp = configuration.Timing.SNTP;
            ntpClient = new NTPClient(  configuration.NTP.Samples, configuration.NTP.History, sntp,
                                        configuration.Timing.Resync, configuration.NTP.Offset, configuration.NTP.Skew / 1E6,
                                        10);
            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});
                
            NTPClientProgressDelegate progress = (o, args) => { if (args.Kind == NTPClientProgressKind.Pinged) ntpClientSampled.Set(); };
            ntpClient.Progress += progress;
            ntpClient.Start();

            //Generate map
            map = new Map(this);
            map.Reconfigure();
            
            ntpClientSampled.WaitOne(3 * sntp); //Timeout is 3 sample periods
            ntpClient.Progress -= progress;

            //Generate server time
            time = new Time(this);
            DateTime dateTime;
            var pac=new StateInfoPacket();
            if (string.IsNullOrEmpty(configuration.Support[0].MJD))
                pac.MJD = HighResolutionDateTime.Now.ToModifiedJulianDay();
            else if (DateTime.TryParse(configuration.Support[0].MJD, out dateTime))
                pac.MJD = new HighResolutionDateTime(dateTime).ToModifiedJulianDay();
            else
                pac.MJD = Convert.ToDouble(configuration.Support[0].MJD);
            pac.Sent = ntpClient.Now.ToDouble;
            time.Update(null, pac);

            //Start TCP listener
            listener =new Listener(this);
            listener.Reconfigure();

            //Start web server
            web = new Web(this);
            web.Reconfigure();

            var configFile = Path.GetFullPath(configurationFileName);
            configurationFileWatcher.Path = Path.GetDirectoryName(configFile);
            configurationFileWatcher.Filter = Path.GetFileName(configFile);
            configurationFileWatcher.NotifyFilter = NotifyFilters.LastWrite;
            configurationFileWatcher.Changed += ConfigurationFile_Changed;
            configurationFileWatcher.EnableRaisingEvents = true;
        }

        void ConfigurationFile_Changed(object sender, FileSystemEventArgs e)
        {
            if (e.ChangeType != WatcherChangeTypes.Changed) return;
            var lastChangeAge = ntpClient.Now - lastConfigChange;
            lastConfigChange = ntpClient.Now;
            configChangeAttempt = lastChangeAge > 5 ? 0 : configChangeAttempt + 1;
            var prefix = configChangeAttempt > 0 ? string.Format("Retry {0}: ", configChangeAttempt) : "";
            OMPServerConfiguration newConfiguration;
            try
            {
                using (var file = File.OpenRead(configurationFileName))
                    try
                    {
                        newConfiguration = ConfigurationSerializer.Deserialize(file) as OMPServerConfiguration;
                        if (newConfiguration.NTP == null) newConfiguration.NTP = new OMPServerConfigurationNTP();
                        if (newConfiguration.HTML == null) newConfiguration.HTML = new OMPServerConfigurationHTML();
                    }
                    catch (InvalidOperationException ex)
                    {
                        log.Error(prefix + "Deserializing changed configuration failed!", ex);
                        return;
                    }
            }
            catch (IOException)
            {
                log.Info(prefix + "Unable to read changed configuration.");
                return;
            }
            log.Info(prefix + "Reading changed configuration...");
            configuration = newConfiguration;
            Reconfigure();
        }
        
        public void Stop()
        {
            configurationFileWatcher.EnableRaisingEvents = false;
            configurationFileWatcher.Changed -= ConfigurationFile_Changed;
            web.Stop();
            listener.Stop();
            ntpClient.Stop();
            map.Clear();
            var skew=(int)(ntpClient.Skew * 1E6);
            if (configuration.NTP.Offset == ntpClient.Offset && configuration.NTP.Skew == skew) return;
            configuration.NTP.Offset = ntpClient.Offset;
            configuration.NTP.Skew = skew;
            using (var file=new FileStream(configurationFileName, FileMode.Create))
                ConfigurationSerializer.Serialize(file, configuration);
        }

        public void Reconfigure()
        {
            //Reconfigure NTP client
            ntpClient.SampleWaitTime = configuration.Timing.SNTP;
            ntpClient.CycleWaitTime = configuration.Timing.Resync;
            var toBeRemoved = new List<NTPServerInfo>(ntpClient.Servers);
            if (configuration.NTP.Items!=null)
                foreach (var s in configuration.NTP.Items)
                {
                    var found = false;
                    foreach(var server in ntpClient.Servers)
                        if (s.Address==server.Address)
                        {
                            toBeRemoved.Remove(server);
                            server.Delay = s.Delay;
                            server.Misses = s.Misses;
                            server.MissesAlarm = s.Alarm;
                            found = true;
                            break;
                        }
                    if (!found) ntpClient.AddServer(new NTPServerInfo(s.Address){Delay = s.Delay,Misses = s.Misses,MissesAlarm = s.Alarm});
                }
            foreach (var server in toBeRemoved) ntpClient.RemoveServer(server);

            map.Reconfigure();
            listener.Reconfigure();
            web.Reconfigure();
        }

        public Time Time
        {
            get { return time; }
        }

        public bool Locked { get; set; }        
    }
}