using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Globalization;
using AirScout.PlaneFeeds.Plugin.MEFContract;
using System.Diagnostics;
using System.Net;
using System.IO;
using System.Threading;
using System.Net.Sockets;
using System.Collections;
using System.Web.Script.Serialization;
using LibADSB;
using System.Xml.Serialization;
using System.Xml.Linq;

namespace AirScout.PlaneFeeds.Plugin.RTL1090
{

    public class RTL1090Settings
    {
        [Browsable(true)]
        [DescriptionAttribute("Server address for raw ADS-B data.\nUse localhost for running on the same machine.")]
        [DefaultValue("localhost")]
        public virtual string Server { get; set; }

        [Browsable(true)]
        [DescriptionAttribute("Server port for raw ADS-B data.\nRTL1090: Port 31001, Dump1090: 30005, ADSBSharp: 47806")]
        [DefaultValue(31001)]
        public virtual int Port { get; set; }

        [Browsable(true)]
        [DescriptionAttribute("Use binary data format for ADS-B data.\nTrue: Use binary format (ADS Beast with MLAT)\nFalse: Use ASCII format (AVR with/without MLAT)")]
        [DefaultValue(true)]
        public virtual bool Binary { get; set; }

        [Browsable(true)]
        [DescriptionAttribute("Use geometric altitudes only (instead of both barometric/geometric) to enhance tracking accuracy.")]
        [DefaultValue(false)]
        public virtual bool UseGeometricAltOnly { get; set; }

        [Browsable(true)]
        [DescriptionAttribute("Report ADS-B messages to console output.")]
        [DefaultValue(true)]
        public virtual bool ReportMessages { get; set; }

        [Browsable(false)]
        [DescriptionAttribute("Marks locally received aircrafts by adding '@' to the call sign")]
        [DefaultValue(false)]
        public virtual bool MarkLocal { get; set; }

        [Browsable(false)]
        [DefaultValue("")]
        [XmlIgnore]
        public string DisclaimerAccepted { get; set; }

        [Browsable(true)]
        [DescriptionAttribute("Save received aircraft positions to file")]
        [DefaultValue(false)]
        public virtual bool SaveToFile { get; set; }

        [Browsable(true)]
        [DescriptionAttribute("Log all received messages to file")]
        [DefaultValue(false)]
        public virtual bool LogMessagesToFile { get; set; }

        [Browsable(false)]
        [DefaultValue("RTL1090Messages.log")]
        [XmlIgnore]
        public string LogFileName { get; set; }

        public RTL1090Settings()
        {
            Default();
            Load(true);
        }

        /// <summary>
        /// Sets all properties to their default value according to the [DefaultValue=] attribute
        /// </summary>
        public void Default()
        {
            // set all properties to their default values according to definition in [DeafultValue=]
            foreach (var p in this.GetType().GetProperties())
            {
                try
                {
                    // initialize all properties with default value if set
                    if (Attribute.IsDefined(p, typeof(DefaultValueAttribute)))
                    {
                        p.SetValue(this, ((DefaultValueAttribute)Attribute.GetCustomAttribute(
                            p, typeof(DefaultValueAttribute)))?.Value, null);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("[" + this.GetType().Name + "]: Cannot set default value of: " + p.Name + ", " + ex.Message);
                }
            }
        }

        /// <summary>
        /// Loads settings from a XML-formatted configuration file into settings.
        /// </summary>
        /// <param name="loadall">If true, ignore the [XmlIgnore] attribute, e.g. load all settings available in the file.<br>If false, load only settings without [XmlIgore] attrbute.</br></param>
        /// <param name="filename">The filename of the settings file.</param>
        public void Load(bool loadall, string filename = "")
        {
            // use standard filename if empty
            // be careful because Linux file system is case sensitive
            if (String.IsNullOrEmpty(filename))
                filename = new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase.Replace(".dll", ".cfg").Replace(".DLL", ".CFG")).LocalPath;
            // do nothing if file not exists
            if (!File.Exists(filename))
                return;
            try
            {
                string xml = "";
                using (StreamReader sr = new StreamReader(File.OpenRead(filename)))
                {
                    xml = sr.ReadToEnd();
                }
                XDocument xdoc = XDocument.Parse(xml);
                PropertyInfo[] properties = this.GetType().GetProperties();
                foreach (PropertyInfo p in properties)
                {
                    if (!loadall)
                    {
                        // check on XmlIgnore attribute, skip if set
                        object[] attr = p.GetCustomAttributes(typeof(XmlIgnoreAttribute), false);
                        if (attr.Length > 0)
                            continue;
                    }
                    try
                    {
                        // get matching element
                        XElement typenode = xdoc.Element(this.GetType().Name);
                        if (typenode != null)
                        {
                            XElement element = typenode.Element(p.Name);
                            if (element != null)
                                p.SetValue(this, Convert.ChangeType(element.Value, p.PropertyType), null);
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("[" + this.GetType().Name + "]: Error while loading property[" + p.Name + " from " + filename + ", " + ex.Message);
                    }
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine("[" + this.GetType().Name + "]: Cannot load settings from " + filename + ", " + ex.Message);
            }
        }

        /// <summary>
        /// Saves settings from settings into a XML-formatted configuration file
        /// </summary>
        /// <param name="saveall">If true, ignore the [XmlIgnore] attribute, e.g. save all settings.<br>If false, save only settings without [XmlIgore] attrbute.</param>
        /// <param name="filename">The filename of the settings file.</param>
        public void Save(bool saveall, string filename = "")
        {
            // use standard filename if empty
            // be careful because Linux file system is case sensitive
            if (String.IsNullOrEmpty(filename))
                filename = new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase.Replace(".dll", ".cfg").Replace(".DLL", ".CFG")).LocalPath;
            XmlAttributeOverrides overrides = new XmlAttributeOverrides();
            if (saveall)
            {
                // ovverride the XmlIgnore attributes to get all serialized
                PropertyInfo[] properties = this.GetType().GetProperties();
                foreach (PropertyInfo p in properties)
                {
                    XmlAttributes attribs = new XmlAttributes { XmlIgnore = false };
                    overrides.Add(this.GetType(), p.Name, attribs);
                }
            }
            try
            {
                using (StreamWriter sw = new StreamWriter(File.Create(filename)))
                {
                    XmlSerializer s = new XmlSerializer(this.GetType(), overrides);
                    s.Serialize(sw, this);
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("[" + this.GetType().Name + "]: Cannot save settings to " + filename + ", " + ex.Message);
            }
        }

    }

    public class RTLMessage
    {
        public string RawMessage = "";
        public DateTime TimeStamp = DateTime.UtcNow;
        public int SignalStrength = 0;
    }

    [Export(typeof(IPlaneFeedPlugin))]
    [ExportMetadata("Name", "PlaneFeedPlugin")]
    public class RTL1090Plugin : IPlaneFeedPlugin
    {
        private RTL1090Settings Settings = new RTL1090Settings();

        ADSBDecoder Decoder = new ADSBDecoder();

        private BackgroundWorker bw_Receciver;

        public string Name
        {
            get
            {
                return "[RawData]           RTL1090 data ";
            }
        }

        public string Info
        {
            get
            {
                return "Raw data feed from simple ADS-B receivers (DVB-T dongles).\n\n" +
                       "(c) AirScout(www.airscout.eu)\n\n" +
                       "Use this feed together with RTL1090.exe or similar software.\n" +
                       "Feed software must output raw data either binary or ASCII format via TCP.\n" +
                       "Use the follwing settings as default:\n\n" +
                       "RTL1090:    port=31001/binary=true\n" +
                       "Dump1090:   port=30005/binary=true\n" + 
                       "ADSBSharp:  port=47806/binary=false"  ;
            }
        }

        public string Version
        {
            get
            {
                return Assembly.GetExecutingAssembly().GetName().Version.ToString();
            }
        }

        public bool HasSettings
        {
            get
            {
                return true;
            }
        }

        public bool CanImport
        {
            get
            {
                return false;
            }
        }

        public bool CanExport
        {
            get
            {
                return false;
            }
        }

        public string Disclaimer
        {
            get
            {
                return "";
            }
        }

        public string DisclaimerAccepted
        {
            get
            {
                return Settings.DisclaimerAccepted;
            }
            set
            {
                Settings.DisclaimerAccepted = value;
            }
        }

        public void ResetSettings()
        {
            Settings.Default();
        }

        public void LoadSettings()
        {
            Settings.Load(true);
        }

        public void SaveSettings()
        {
            Settings.Save(true);
        }

        public object GetSettings()
        {
            return this.Settings;
        }

        public void ImportSettings()
        {
            // nothing to do
        }

        public void ExportSettings()
        {
            // nothing to do
        }

        public void Start(PlaneFeedPluginArgs args)
        {
            // start receiver thread
            if (!bw_Receciver.IsBusy)
            {
                if (Settings.LogMessagesToFile)
                {
                    try
                    {
                        File.WriteAllText(Path.Combine(args.TmpDirectory, Settings.LogFileName), "RTL1090 logging started: " + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"));
                    }
                    catch (Exception ex)
                    {

                    }
                }
                bw_Receciver.RunWorkerAsync(args);
            }
        }

        public PlaneFeedPluginPlaneInfoList GetPlanes(PlaneFeedPluginArgs args)
        {
            PlaneFeedPluginPlaneInfoList planes = new PlaneFeedPluginPlaneInfoList();
            // time to report planes
            ArrayList list = Decoder.GetPlanes();
            if (list.Count > 0)
            {
                // convert to plane info list
                foreach (ADSBInfo info in list)
                {
                    PlaneFeedPluginPlaneInfo plane = new PlaneFeedPluginPlaneInfo();
                    plane.Time = info.Timestamp;
                    plane.Hex = info.ICAO24;
                    // mark call with "@" if option is enabled
                    plane.Call = (Settings.MarkLocal) ? "@" + info.Call : info.Call;
                    plane.Lat = info.Lat;
                    plane.Lon = info.Lon;
                    // use geometric altitude, if enabled and available
                    if (Settings.UseGeometricAltOnly)
                    {
                        if (info.GeoMinusBaro != int.MinValue)
                            plane.Alt = info.BaroAlt + info.GeoMinusBaro;
                        else
                            plane.Alt = int.MinValue;
                    }
                    else
                    {
                        plane.Alt = info.BaroAlt + info.GeoMinusBaro;
                    }
                    plane.Speed = info.Speed;
                    plane.Track = info.Heading;
                    plane.Reg = "[unknown]";
                    plane.Type = "[unknown]";
                    plane.Manufacturer = "[unknown]";
                    plane.Model = "[unknown]";
                    plane.Category = 0;

                    // add only valid positions
                    if (plane.Alt > 0)
                    {
                        planes.Add(plane);
                    }
                }
                // save raw data to file if enabled
                if (Settings.SaveToFile)
                {
                    JavaScriptSerializer js = new JavaScriptSerializer();
                    string json = js.Serialize(planes);
                    using (StreamWriter sw = new StreamWriter(args.TmpDirectory + Path.DirectorySeparatorChar + this.GetType().Name + "_" + DateTime.UtcNow.ToString("yyyy-MM-dd HH_mm_ss") + ".json"))
                    {
                        sw.WriteLine(json);
                    }
                }

            }
            Console.WriteLine("[" + this.GetType().Name + "]: Returning " + planes.Count + " planes");
            return planes;
        }

        public void Stop(PlaneFeedPluginArgs args)
        {
            while (bw_Receciver.IsBusy)
            {
                bw_Receciver.CancelAsync();
            }
            Settings.Save(true);
        }

        // end of Interface

        public RTL1090Plugin()
        {
            bw_Receciver = new BackgroundWorker();
            bw_Receciver.DoWork += new System.ComponentModel.DoWorkEventHandler(this.bw_Receiver_DoWork);
            bw_Receciver.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.bw_Receiver_ProgressChanged);
            bw_Receciver.WorkerReportsProgress = true;
            bw_Receciver.WorkerSupportsCancellation = true;
        }

        private void bw_Receiver_DoWork(object sender, DoWorkEventArgs e)
        {
            PlaneFeedPluginArgs args = (PlaneFeedPluginArgs)e.Argument;

            Thread.CurrentThread.Priority = ThreadPriority.Highest;
            StreamReader sr = null;
            TcpClient client = null;
            RTLMessage msg = null;
            // outer loop
            do
            {
                try
                {
                    // setup TCP listener
                    client = new TcpClient();
                    // set receive timeout to 1s
                    client.ReceiveTimeout = 1000;
                    string server = Settings.Server;
                    client.Connect(server, Settings.Port);
                    sr = new StreamReader(client.GetStream());
                    // inner loop
                    // receive messages in a loop
                    do
                    {
                        if (Settings.Binary)
                            msg = ReceiveBinaryMsg(sr.BaseStream);
                        else
                            msg = ReceiveAVRMsg(sr);
                        // decode the message
                        if (msg.RawMessage.StartsWith("*") && msg.RawMessage.EndsWith(";"))
                        {
                            // try to decode the message
                            string info = "";
                            string line = "";
                            try
                            {
                                info = "[" + this.GetType().Name + "]: " + msg.RawMessage + "-- > ";
                                Console.Write(info);
                                line = info;
                                info = Decoder.DecodeMessage(msg.RawMessage, msg.TimeStamp, Settings.UseGeometricAltOnly);
                                line = line + info + "\n";
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine(ex.Message);
                            }
                            Console.WriteLine(info);
                            
                            if (Settings.LogMessagesToFile)
                            {
                                try
                                {
                                    File.AppendAllText(Path.Combine(args.TmpDirectory, Settings.LogFileName), line);
                                }
                                catch (Exception ex)
                                {

                                }
                            }
                        }
                    }
                    while ((msg != null) && !bw_Receciver.CancellationPending);
                }
                catch (Exception ex)
                {
                    // report error
                    Console.WriteLine("Error reading from TCP connection: " + ex.Message);
                    // wait 10 sec
                    int i = 0;
                    while ((i < 10) && !bw_Receciver.CancellationPending)
                    {
                        Thread.Sleep(1000);
                        i++;
                    }
                }
                finally
                {
                    // try to close the stream and TCP client
                    try
                    {
                        if (sr != null)
                            sr.Close();
                    }
                    catch
                    {
                    }
                    try
                    {
                        if (client != null)
                            client.Close();
                    }
                    catch
                    {
                    }
                }
            }
            while (!bw_Receciver.CancellationPending);
        }

        private void bw_Receiver_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
        }

        private RTLMessage ReceiveBinaryMsg(Stream stream)
        {
            // read Mode-S beast binary input
            string RTL = null;
            int signal_strength = 0;
            long nanosec = 0;
            long daysec = 0;
            DateTime timestamp = DateTime.UtcNow;
            byte[] buffer = new byte[23];
            // wait for escape character
            DateTime start = DateTime.UtcNow;
            DateTime stop = DateTime.Now;
            do
            {
                stream.Read(buffer, 0, 1);
                //                System.Console.WriteLine(BitConverter.ToString(buffer,0,1));
                if (buffer[0] == 0x1A)
                {
                    // read next character
                    stream.Read(buffer, 1, 1);
                    switch (buffer[1])
                    {
                        case 0x31:
                            // do not decode
                            RTL = null;
                            break;
                        case 0x32:
                            // 7 byte short frame
                            // read timestamp
                            stream.Read(buffer, 2, 6);
                            nanosec = ((buffer[4] & 0x3f) << 24) |
                                (buffer[5] << 16) |
                                (buffer[6] << 8) |
                                (buffer[7]);
                            daysec = (buffer[2] << 10) |
                                (buffer[3] << 2) |
                                (buffer[4] >> 6);
                            timestamp = DateTime.Today.AddSeconds(daysec);
                            timestamp = timestamp.AddMilliseconds(nanosec / 1000);
                            // plausibility check
                            if (Math.Abs((DateTime.Now - timestamp).Seconds) > 10)
                            {
                                // time difference > 10sec --> discard timestamp
                                timestamp = DateTime.UtcNow;
                            }
                            // read signal strength
                            stream.Read(buffer, 8, 1);
                            // plausibility check
                            if (Math.Abs((DateTime.Now - timestamp).Seconds) > 10)
                            {
                                // time difference > 10sec --> discard timestamp
                                timestamp = DateTime.UtcNow;
                            }
                            signal_strength = buffer[8];
                            // read frame
                            stream.Read(buffer, 9, 7);
                            // convert to AVR string
                            RTL = BitConverter.ToString((byte[])buffer, 9, 7).Replace("-", String.Empty);
                            RTL = "*" + RTL + ";";
                            break;
                        case 0x33:
                            // 14 byte long frame
                            // read timestamp
                            stream.Read(buffer, 2, 6);
                            nanosec = ((buffer[4] & 0x3f) << 24) |
                                (buffer[5] << 16) |
                                (buffer[6] << 8) |
                                (buffer[7]);

                            daysec = (buffer[2] << 10) |
                                (buffer[3] << 2) |
                                (buffer[4] >> 6);
                            timestamp = DateTime.Today.AddSeconds(daysec);
                            timestamp = timestamp.AddMilliseconds(nanosec / 1000);
                            // plausibility check
                            if (Math.Abs((DateTime.Now - timestamp).Seconds) > 10)
                            {
                                // time difference > 10sec --> discard timestamp
                                timestamp = DateTime.UtcNow;
                            }
                            // read signal strength
                            stream.Read(buffer, 8, 1);
                            signal_strength = buffer[8];
                            // read frame
                            stream.Read(buffer, 9, 14);
                            // convert to AVR string
                            RTL = BitConverter.ToString((byte[])buffer, 9, 14).Replace("-", String.Empty);
                            RTL = "*" + RTL + ";";
                            break;
                        default:
                            // false decode
                            RTL = null;
                            break;
                    }
                }
                // check for timeout 10sec
                stop = DateTime.UtcNow;
                if (stop - start > new TimeSpan(0, 0, 10))
                    throw new TimeoutException();
            }
            //            while ((RTL == null) && !this.CancellationPending);
            while (RTL == null);
            if (RTL == null)
                return null;
            RTLMessage msg = new RTLMessage();
            msg.RawMessage = RTL;
            msg.TimeStamp = timestamp;
            msg.SignalStrength = signal_strength;
            return msg;
        }

        private RTLMessage ReceiveAVRMsg(StreamReader sr)
        {
            RTLMessage msg = new RTLMessage();
            // read AVR format input
            msg.RawMessage = sr.ReadLine();
            if (msg.RawMessage.StartsWith("*"))
            {
                // standard AVR message
                // no timestamp in telegram --> set timestamp after reading
                msg.TimeStamp = DateTime.UtcNow;
                msg.SignalStrength = 0;
            }
            else if (msg.RawMessage.StartsWith("@"))
            {
                // extended AVR message wit MLAT
                // convert into standard message format
                // extract time string
                string time = msg.RawMessage.Substring(1, 12);
                // TODO: interprete the MLAT timestamp!
                msg.TimeStamp = DateTime.UtcNow;
                msg.RawMessage = "*" + msg.RawMessage.Remove(0, 13);
            }
            return msg;
        }
    }


    /// <summary>
    /// //////////////////////////////////////////// Helpers ////////////////////////////////////////////
    /// </summary>

    public static class UnitConverter
    {
        public static double ft_m(double feet)
        {
            return feet / 3.28084;
        }

        public static double m_ft(double m)
        {
            return m * 3.28084;
        }

        public static double kts_kmh(double kts)
        {
            return kts * 1.852;
        }

        public static double kmh_kts(double kmh)
        {
            return kmh / 1.852;
        }

        public static double km_mi(double km)
        {
            return km * 1.609;
        }

        public static double mi_km(double mi)
        {
            return mi / 1.609;
        }
    }

}
