using System;
using System.Collections.Generic;
using System.Xml;
using System.Globalization;
using System.Diagnostics;

using System.Linq;
using Serilog;

using pavesys_iRAP.Helper;

namespace pavesys_iRAP.Business
{

    public class LogXML
    {
        public int Id { get; set; }
        public int Odometro { get; set; }
        public int Velocidade { get; set; }
        public decimal OdometroTrecho { get; set; }
        public int ExtLog { get; set; }
        public DateTime DataHora { get; set; }
        public int TempoLog { get; set; }
        public TempoCamera TmpCamera { get; set; }
        public FrameCamera FrmCamera { get; set; }
        public GPS GPS { get; set; }

        public (decimal, decimal) LonLatGPMRC { get; set; }
        public (decimal, decimal) LonLatGPGGA { get; set; }

    }

    public struct TempoCamera
    {
        public decimal Frente { get; set; }
        public decimal Tras { get; set; }
    }

    public struct FrameCamera
    {
        public int Frente { get; set; }

        public int Tras { get; set; }
    }

    public struct GPS
    {
        public decimal Velocidade { get; set; }
        public int Odometro { get; set; }
        public decimal Y { get; set; }
        public decimal X { get; set; }
        public decimal Z { get; set; }
        public string Azi { get; set; }
        public int Erro { get; set; }
        public int Sat { get; set; }

        public string GPRMC { get; set; }
        public string GPGGA { get; set; }
        public string GPGSA { get; set; }
    }


    public class InputXML
    {

        public List<LogXML> logs;
        //Possible source of incosistent gps tracking is the miss conversion of latitude/longitude 
        //When reading from the gprmc/gpgga we're ignoring the seconds using only degrees and minutes.
        //the log files provided do the same.
        public decimal LongitudeParser(string lon)
        {
            //Longitude (DDDmm.mm)
            //04851.68090
            var degrees = int.Parse(lon.Substring(0, 3));
            var minutes = decimal.Parse(lon.Substring(3), new NumberFormatInfo { NumberDecimalSeparator = "." });
            return Math.Round(degrees + (minutes / 60), 6);
        }
        public decimal LatitudeParser(string lat)
        {
            //Latitude (DDmm.mm)
            //2604.01409
            var degrees = int.Parse(lat.Substring(0, 2));
            var minutes = decimal.Parse(lat.Substring(2), new NumberFormatInfo { NumberDecimalSeparator = "." });
            return Math.Round(degrees + (minutes / 60), 6);
        }
        public (decimal Longitude, decimal Lagitude) ExtractCoordinates(string NMEAsentence)
        {
            decimal longitude;
            decimal latitude;
            //GPRMC sentence, aceita gnrmc porque alguns xmls da pavesys estavam assim.
            if (NMEAsentence.StartsWith("$GPRMC") || NMEAsentence.StartsWith("$GNRMC"))
            {

                string[] Fields = NMEAsentence.Split(',');

                longitude = Fields[6] switch
                {
                    "E" => LongitudeParser(Fields[5]),
                    _ => -LongitudeParser(Fields[5])
                };

                latitude = Fields[4] switch
                {
                    "N" => LatitudeParser(Fields[3]),
                    _ => -LatitudeParser(Fields[3])
                };

                return (longitude, latitude);


            }
            // GPGGA sentence aceita GNGGA porque alguns xmls da pavesys estavam assim.
            else if (NMEAsentence.StartsWith("$GPGGA") || NMEAsentence.StartsWith("$GNGGA"))
            {
                string[] Fields = NMEAsentence.Split(',');
                longitude = Fields[5] switch
                {
                    "E" => LongitudeParser(Fields[4]),
                    _ => -LongitudeParser(Fields[4])
                };

                latitude = Fields[3] switch
                {
                    "N" => LatitudeParser(Fields[2]),
                    _ => -LatitudeParser(Fields[2])
                };

                return (longitude, latitude);
            }

            throw new Exception("Failed to Read NMEASentence");
        }
        //Returns the distance between two coordinates in meters
        //Changed to vincenty formula 
        public static double CalculateHaversine(double lat1, double lon1, double lat2, double lon2)
        {
            double rad(double angle) => angle * 0.017453292519943295769236907684886127d; // = angle * Math.Pi / 180.0d
            double havf(double diff) => Math.Pow(Math.Sin(rad(diff) / 2d), 2); // = sin²(diff / 2)
            return (12745.6 * Math.Asin(Math.Sqrt(havf(lat2 - lat1) + Math.Cos(rad(lat1)) * Math.Cos(rad(lat2)) * havf(lon2 - lon1)))) * 1000; // earth radius 6.372,8km x 2 = 12745.6
        }

        public static double GetVincentyDistance(double lat1, double lon1, double lat2, double lon2)
        {
            double a = 6378137, b = 6356752.314245, f = 1 / 298.257223563;

            double L = Math.PI / 180 * (lon2 - lon1);

            double U1 = Math.Atan(Math.Tan(Math.PI / 180 * lat1) / (1 - f));

            double U2 = Math.Atan(Math.Tan(Math.PI / 180 * lat2) / (1 - f));

            double sinU1 = Math.Sin(U1), cosU1 = Math.Cos(U1);

            double sinU2 = Math.Sin(U2), cosU2 = Math.Cos(U2);

            double cosSqAlpha, sinSigma, cos2SigmaM, cosSigma, sigma;

            double lambda = L, lambdaP, iterLimit = 100;

            do
            {
                double sinLambda = Math.Sin(lambda), cosLambda = Math.Cos(lambda);

                sinSigma = Math.Sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) +
                                     (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) *
                                     (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));

                if (sinSigma == 0) return 0;

                cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;

                sigma = Math.Atan2(sinSigma, cosSigma);

                double sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;

                cosSqAlpha = 1 - sinAlpha * sinAlpha;

                cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;

                double C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha));

                lambdaP = lambda;

                lambda = L + (1 - C) * f * sinAlpha *
                         (sigma + C * sinSigma *
                          (cos2SigmaM + C * cosSigma *
                           (-1 + 2 * cos2SigmaM * cos2SigmaM)
                          )
                         );

            } while (Math.Abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0);

            if (iterLimit == 0) return 0;

            double uSq = cosSqAlpha * (a * a - b * b) / (b * b);

            double A = 1 + uSq / 16384 *
                        (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));

            double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));

            double deltaSigma =
                B * sinSigma *
                (cos2SigmaM + B / 4 *
                 (cosSigma *
                  (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM *
                   (-3 + 4 * sinSigma * sinSigma) *
                   (-3 + 4 * cos2SigmaM * cos2SigmaM)
                 )
                );

            double s = b * A * (sigma - deltaSigma);
            return s;
        }

        public InputXML(string xmlFileName)
        {
            //Read Correctly logs will be inserted here
            logs = new List<LogXML>();
            var commaSeparator = new NumberFormatInfo() { NumberDecimalSeparator = "," };
            var dotSeparator = new NumberFormatInfo() { NumberDecimalSeparator = "." };
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(xmlFileName);

            // Get all 'Log' nodes under 'Logs'
            XmlNodeList logNodes = xmlDoc.SelectNodes("//Logs/Log");
            for (int i = 0; i < logNodes.Count; i++)
            {
                LogXML log = new LogXML();

                try
                {

                    //Variaveis usadas para receber as informações do XML
                    FrameCamera frmCamer = new FrameCamera();
                    TempoCamera TmpCamera = new TempoCamera();
                    GPS logGPS = new GPS();
                    //Recebe informações do Node LOG
                    log.Id = int.Parse(logNodes[i].Attributes[0].Value);
                    log.Odometro = int.Parse(logNodes[i].Attributes[1].Value);
                    log.Velocidade = int.Parse(logNodes[i].Attributes[2].Value);
                    //OdometroTrecho 
                    log.OdometroTrecho = log.Odometro;  // Should be multiplied by the pulse coefficient later.
                    log.ExtLog = int.Parse(logNodes[i].Attributes[4].Value);
                    log.DataHora = DateTime.ParseExact(
                        logNodes[i].Attributes[5].Value,
                        "yyyy-MM-ddTHH:mm:ss.ffffffzzz",
                        System.Globalization.CultureInfo.InvariantCulture);
                    log.TempoLog = int.Parse(logNodes[i].Attributes[6].Value);
                    //Recebe node TempoCamera
                    TmpCamera.Frente = decimal.Parse(logNodes[i].ChildNodes[0].Attributes[0].Value.Replace(".", String.Empty), commaSeparator);
                    TmpCamera.Tras = decimal.Parse(logNodes[i].ChildNodes[0].Attributes[1].Value.Replace(".", String.Empty), commaSeparator);
                    log.TmpCamera = TmpCamera;
                    //Recebe node FrameCamera
                    frmCamer.Frente = int.Parse(logNodes[i].ChildNodes[1].Attributes[0].Value);
                    frmCamer.Tras = int.Parse(logNodes[i].ChildNodes[1].Attributes[1].Value);
                    log.FrmCamera = frmCamer;
                    //Recebe node GPS
                    logGPS.Velocidade = decimal.Parse(logNodes[i].ChildNodes[2].Attributes[0].Value, dotSeparator);
                    logGPS.Odometro = int.Parse(logNodes[i].ChildNodes[2].Attributes[1].Value);

                    logGPS.Y = decimal.Parse(logNodes[i].ChildNodes[2].Attributes[2].Value, dotSeparator);
                    logGPS.X = decimal.Parse(logNodes[i].ChildNodes[2].Attributes[3].Value, dotSeparator);
                    logGPS.Z = decimal.Parse(logNodes[i].ChildNodes[2].Attributes[4].Value, dotSeparator);
                    logGPS.Azi = logNodes[i].ChildNodes[2].Attributes[5].Value; // Assuming Azi is a string
                    logGPS.Erro = int.Parse(logNodes[i].ChildNodes[2].Attributes[6].Value);
                    logGPS.Sat = int.Parse(logNodes[i].ChildNodes[2].Attributes[7].Value);
                    logGPS.GPRMC = logNodes[i].ChildNodes[2].Attributes[8].Value;
                    logGPS.GPGGA = logNodes[i].ChildNodes[2].Attributes[9].Value;
                    logGPS.GPGSA = logNodes[i].ChildNodes[2].Attributes[10].Value;
                    log.GPS = logGPS;
                    log.LonLatGPMRC = ExtractCoordinates(log.GPS.GPRMC);
                    log.LonLatGPGGA = ExtractCoordinates(log.GPS.GPGGA);
                    logs.Add(log);

                }
                catch (Exception ex)
                {
                    Log.Error(ex, "Error Reading XML file");
                    Log.Error($"Error at log id {log.Id}");
                    DialogHelper.ShowOkDialog("ErrorXml", "ErrorReadingXml");
                    break;
                }
            }
            //OutputRepeatedCoordinatesError(logs);


        }
        public bool OutputRepeatedCoordinatesError(List<LogXML> logs, out Dictionary<(decimal, decimal), List<int>> errorDictionary)
        {
            Dictionary<(decimal, decimal), List<int>> errorDic = new();
            foreach (var log in logs)
            {
                //List to log possible errors of repeated coordinates
                List<int>? errorList;
                //Insert repeated longitudes in a list to check if there's an gps error.
                if (errorDic.TryGetValue(log.LonLatGPGGA, out errorList))
                {
                    errorList.Add(log.Id);
                }
                else
                {
                    errorList = new List<int>();
                    errorList.Add(log.Id);
                    errorDic.Add(log.LonLatGPGGA, errorList);
                }
            }
            //----Used to log to a file ---
            //StringBuilder sb = new StringBuilder();
            //foreach (var list in errorDic.Values)
            //{
            //    if (list.Count >= 20)
            //    {

            //        sb.Append("Log IDs:");
            //        sb.Append(list[0] + " - " + list.Last());
            //        sb.AppendLine();
            //    }
            //}
            //File.WriteAllText(Path.Combine("logs", "errorXML.txt"), sb.ToString());
            var numError = errorDic.Where(list => list.Value.Count >= 3 && list.Value.Count <= ConfigManager.GetInputErrorLimit()).Count();
            var numErrorOverLimit = errorDic.Where(list => list.Value.Count >= ConfigManager.GetInputErrorLimit()).Count();
            if (numError >= 1 || numErrorOverLimit >= 1)
            {
                errorDictionary = errorDic;
                return true;
                
            }
            errorDictionary = null;
            return false;
        }
        public void calculatePulse(List<LogXML> logs, int startPhoto, int endPhoto, out decimal displacement, out decimal pulse, out decimal endingKm)
        {
            if (logs.LastOrDefault() == null)
            {
                throw new Exception("Error on xml parsing");
            }
            //Calculate pulse coefficient and OdometroTrecho:
            double? lastOdometer = logs.LastOrDefault().Odometro;

            double sumCoordinateDifference = 0;
            int photoDifference = endPhoto - startPhoto;

            List<decimal> pulses = new();
            List<double> sumsCoordinateDifference = new();



            
            var sumHaversine = 0D;
            for (int i = 0; i < logs.Count - 1; i++)
            {
                var firstCoordinate = logs[i].LonLatGPGGA;
                var scndCoordinate = logs[i + 1].LonLatGPGGA;
                sumCoordinateDifference += GetVincentyDistance((double)firstCoordinate.Item2, (double)firstCoordinate.Item1,
                    (double)scndCoordinate.Item2, (double)scndCoordinate.Item1);
                //sumHaversine += CalculateHaversine((double)firstCoordinate.Item2, (double)firstCoordinate.Item1,
                //    (double)scndCoordinate.Item2, (double)scndCoordinate.Item1);
            }

            //Se as fotos foram escolhidas
            if (photoDifference == 0)
                pulse = (decimal)(sumCoordinateDifference / lastOdometer);

            else
                //Teoricamente iriamos dividir pela diferença das fotos mas isso deixava o cálculo meio esquisito.
                pulse = (decimal)(sumCoordinateDifference / lastOdometer);
            
            foreach (var log in logs)
            {
                log.OdometroTrecho = pulse * log.Odometro;
            }
            var startTrecho = logs.Where(log => log.Odometro == startPhoto).First();

            foreach (var log in logs)
            {
                if (log.Id != startTrecho.Id)
                    log.OdometroTrecho -= startTrecho.OdometroTrecho;
            }
            startTrecho.OdometroTrecho = 0;
            displacement = -logs.First().OdometroTrecho;

            if (endPhoto != 0)// if photos were selected
                endingKm = logs.Where(log => log.Odometro == endPhoto).First().OdometroTrecho / 1000;
            //endingKm = photoDifference / 1000M;
            else
            {
                endingKm = logs.Last().OdometroTrecho / 1000;
            }
        }
    }
}
