using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.IO.Ports;
using System.Net.Sockets;
using Elwig.Dialogs;
using System.Text;
using System.Numerics;
using Elwig.Models.Entities;
using Elwig.Helpers.Billing;
using System.Runtime.InteropServices;
using System.Net.Http;
using System.Text.Json.Nodes;
using System.IO;
using MailKit.Net.Smtp;
using MailKit.Security;
using OpenTK.Compute.OpenCL;

namespace Elwig.Helpers {
    public static partial class Utils {

        public static readonly Encoding UTF8 = new UTF8Encoding(false, true);

        public static int CurrentYear => DateTime.Now.Year;
        public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
        public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 7 ? 1 : 0);
        public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1);

        [GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
        private static partial Regex GeneratedSerialRegex();
        public static readonly Regex SerialRegex = GeneratedSerialRegex();

        [GeneratedRegex("^tcp://([A-Za-z0-9._-]+):([0-9]+)$", RegexOptions.Compiled)]
        private static partial Regex GeneratedTcpRegex();
        public static readonly Regex TcpRegex = GeneratedTcpRegex();

        [GeneratedRegex(@"^(-?(0?[1-9]|[12][0-9]|3[01])\.(0?[1-9]|1[0-2])\.([0-9]{4})?-?){1,2}$", RegexOptions.Compiled)]
        private static partial Regex GeneratedFromToDateRegex();
        public static readonly Regex DateFromToRegex = GeneratedFromToDateRegex();

        [GeneratedRegex(@"^([0-9]+([\.,][0-9]+)?)?-([0-9]+([\.,][0-9]+)?)?$", RegexOptions.Compiled)]
        private static partial Regex GeneratedFromToRegex();
        public static readonly Regex FromToRegex = GeneratedFromToRegex();

        [GeneratedRegex(@"^([0-9]{1,2}:[0-9]{2})?-([0-9]{1,2}:[0-9]{2})?$", RegexOptions.Compiled)]
        private static partial Regex GeneratedFromToTimeRegex();
        public static readonly Regex FromToTimeRegex = GeneratedFromToTimeRegex();

        [GeneratedRegex(@"^(.*?) +([0-9].*)$", RegexOptions.Compiled)]
        private static partial Regex GeneratedAddressRegex();
        public static readonly Regex AddressRegex = GeneratedAddressRegex();

        [GeneratedRegex(@"[^A-Za-z0-9ÄÜÖẞäöüß-]+")]
        private static partial Regex GeneratedInvalidFileNamePartsRegex();
        public static readonly Regex InvalidFileNamePartsRegex = GeneratedInvalidFileNamePartsRegex();

        public static readonly string GroupSeparator = "\u202F";
        public static readonly string UnitSeparator = "\u00A0";

        public static readonly KeyValuePair<string, string>[] PhoneNrTypes = [
            new("landline", "Tel.-Nr. (Festnetz)"),
            new("mobile", "Tel.-Nr. (mobil)"),
            new("fax", "Fax-Nr."),
        ];

        public static string PhoneNrTypeToString(string type) {
            return PhoneNrTypes.Where(t => t.Key == type).Select(t => t.Value).FirstOrDefault(type);
        }

        private static readonly string[] TempWildcards = ["*.html", "*.pdf", "*.exe"];

        private static readonly ushort[] Crc16ModbusTable = [
            0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
            0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
            0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
            0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
            0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
            0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
            0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
            0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
            0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
            0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
            0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
            0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
            0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
            0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
            0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
            0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
            0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
            0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
            0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
            0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
            0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
            0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
            0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
            0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
            0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
            0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
            0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
            0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
            0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
            0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
            0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
            0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
        ];

        public static SerialPort OpenSerialConnection(string connection) {
            var m = SerialRegex.Match(connection);
            if (!m.Success)
                throw new ArgumentException("Invalid connection string for scheme \"serial\"");

            var stop = m.Groups[6].Value;
            var parity = m.Groups[5].Value.ToUpper();
            var data = m.Groups[4].Value;
            var port = new SerialPort() {
                PortName = m.Groups[1].Value,
                BaudRate = int.Parse(m.Groups[2].Value),
                Parity = parity == "E" ? Parity.Even :
                         parity == "O" ? Parity.Odd :
                         parity == "M" ? Parity.Mark :
                         parity == "S" ? Parity.Space :
                                         Parity.None,
                DataBits = data == "" ? 8 : int.Parse(data),
                StopBits = (StopBits)(stop == "" ? 1 : stop == "1.5" ? 3 : stop[0] - '0'),
                Handshake = Handshake.None,
                WriteTimeout = 250,
                ReadTimeout = 11000,
            };
            port.Open();
            return port;
        }

        public static TcpClient OpenTcpConnection(string connection) {
            var m = TcpRegex.Match(connection);
            if (!m.Success)
                throw new ArgumentException("Invalid connection string for scheme \"tcp\"");

            var client = new TcpClient() {
                SendTimeout = 250,
                ReceiveTimeout = 11000,
            };
            client.Connect(m.Groups[1].Value, int.Parse(m.Groups[2].Value));
            return client;
        }

        public static int Modulo(string a, int b) {
            if (a.Length == 0 || !a.All(char.IsAsciiDigit)) {
                throw new ArgumentException("First argument has to be a decimal string");
            } else if (b < 2) {
                throw new ArgumentException("Second argument has to be greater than 1");
            }
            return a.Select(ch => ch - '0').Aggregate((sum, n) => (sum * 10 + n) % b);
        }

        public static ushort CalcCrc16Modbus(byte[] data) {
            // https://www.modbustools.com/modbus_crc16.htm
            byte temp;
            ushort crc = 0xFFFF;
            foreach (byte b in data) {
                temp = (byte)(b ^ crc);
                crc >>= 8;
                crc ^= Crc16ModbusTable[temp];
            }
            return crc;
        }

        public static ushort CalcCrc16Modbus(string data) {
            return CalcCrc16Modbus(Encoding.ASCII.GetBytes(data));
        }

        public static string FormatIban(string iban) {
            return Regex.Replace(iban.Trim(), ".{4}", "$0 ").Trim();
        }

        public static void RunBackground(string title, Func<Task> a) {
            Task.Run(async () => {
                try {
                    await a();
                } catch (Exception e) {
                    MessageBox.Show(e.ToString(), title, MessageBoxButton.OK, MessageBoxImage.Error);
                }
            });
        }

        public static void MailTo(string emailAddress) {
            MailTo(new string[] { emailAddress });
        }

        public static void MailTo(IEnumerable<string> emailAddresses) {
            Process.Start(new ProcessStartInfo() {
                FileName = $"mailto:{string.Join(",%20", emailAddresses)}",
                UseShellExecute = true,
            });
        }

        public static double KmwToOe(double kmw) {
            return Math.Round(kmw * (4.54 + 0.022 * kmw), 0);
        }

        public static double OeToKmw(double oe) {
            return Math.Round((-4.54 + Math.Sqrt(4.54 * 4.54 - 4 * 0.022 * -oe)) / (2 * 0.022), 1);
        }

        public static decimal DecFromDb(long value, byte precision) {
            bool neg = value < 0;
            if (neg) value = -value;
            return new((int)(value & 0xFFFFFFFF), (int)((value >> 32) & 0x7FFFFFFF), 0, neg, precision);
        }

        public static long DecToDb(decimal value, byte precision) {
            return (long)decimal.Round(value * (decimal)Math.Pow(10, precision), 0);
        }

        public static int GetAge(DateOnly birthday) {
            var today = DateTime.Today;
            var a = (today.Year * 100 + today.Month) * 100 + today.Day;
            var b = (birthday.Year * 100 + birthday.Month) * 100 + birthday.Day;
            return (a - b) / 10000;
        }

        public static int GetSearchScore(IEnumerable<string?> words, IEnumerable<string> searchKeywords) {
            searchKeywords = searchKeywords.Where(s => s.Length >= 2 || (s.Length > 0 && s.All(c => char.IsAsciiDigit(c))));
            if (!searchKeywords.Any())
                return 0;

            return searchKeywords
                .Select(k => {
                    k = k.ToLower();
                    var scores = words.Select(w => {
                        w = w?.ToLower();
                        var p = w?.ToLower()?.Split(" ");
                        if (w == null || p == null) {
                            return 0;
                        } else if (k == w) {
                            return 4 + k.Length;
                        } else if (p.Any(a => a == k)) {
                            return 3 + k.Length;
                        } else if (p.Any(a => a.StartsWith(k))) {
                            return 2 + k.Length;
                        } else if (w.Contains(k)) {
                            return 1 + k.Length;
                        } else {
                            return 0;
                        }
                    });
                    return scores.Max() + scores.Count(s => s > 0);
                })
                .Sum();
        }

        public static (int, string?)? ShowManualWeighingDialog(string? reason = null) {
            var d = new ManualWeighingDialog(reason);
            return d.ShowDialog() == true ? (d.Weight, d.Reason) : null;
        }

        public static int? ShowAbwertenDialog(string lsnr, string name, int weight) {
            var d = new AbwertenDialog(lsnr, name, weight);
            return d.ShowDialog() == true ? d.Weight : null;
        }

        public static double? ShowLinearPriceIncreaseDialog() {
            var d = new LinearPriceIncreaseDialog();
            return d.ShowDialog() == true ? d.Price : null;
        }

        public static string? ShowDeliveryExtractionDialog(string lsnr, string name, bool single, IEnumerable<string> lsnrs) {
            var d = new DeliveryExtractionDialog(lsnr, name, single, lsnrs);
            return d.ShowDialog() == true ? d.AddTo : null;
        }

        public static Footer GenerateFooter(string lineBreak, string seperator) {
            return new Footer(lineBreak, seperator);
        }

        public class Footer {
            private string Text = "";
            private readonly string LineBreak;
            private readonly string Seperator;
            private bool FirstLine = true;
            private bool FirstItemInLine = true;

            public Footer(string lineBreak, string seperator) {
                LineBreak = lineBreak;
                Seperator = seperator;
            }

            public Footer Item(string? text) {
                if (text == null) return this;
                Text += FirstItemInLine ? (FirstLine ? "" : LineBreak) : Seperator;
                Text += text;
                FirstLine = false;
                FirstItemInLine = false;
                return this;
            }

            public Footer Item(string name, string? text) {
                return text == null ? this : Item($"{name}: {text}");
            }

            public Footer NextLine() {
                FirstItemInLine = true;
                return this;
            }

            public override string ToString() {
                return Text;
            }
        }

        public static string GetSign<T>(T number) where T : INumber<T>
            => T.Sign(number) switch {
                < 0 => "\u2212",  // minus
                0 => "\u00b1",  // plus minus
                > 0 => "+",
            };

        public static double AggregateDeliveryPartsKmw(IEnumerable<DeliveryPart> parts)
            => parts.Aggregate(
                (Weight: 0, Kmw: 0.0),
                (sum, item) => (
                    sum.Weight + item.Weight,
                    (sum.Kmw * sum.Weight + item.Kmw * item.Weight) / (sum.Weight + item.Weight)
                ),
                sum => sum.Kmw
            );

        public static string GenerateLsNr(Delivery d) => GenerateLsNr(d.Date, d.ZwstId, d.LNr);

        public static string GenerateLsNr(DateOnly date, string zwstid, int lnr) => $"{date:yyyyMMdd}{zwstid}{lnr:000}";

        public static (string, string?) SplitAddress(string address) {
            var m = AddressRegex.Match(address);
            return (m.Groups[1].Value, m.Groups[2].Value);
        }

        public static (string, string?) SplitName(string fullName, string? familyName) {
            if (familyName == null || familyName == "") return (fullName, null);
            var p0 = fullName.IndexOf(familyName, StringComparison.CurrentCultureIgnoreCase);
            if (p0 == -1) return (fullName, null);
            var p1 = fullName.IndexOf(" und ");
            var p2 = fullName.ToLower().LastIndexOf(" und ");
            if (p1 != p2) {
                if (p0 > p1) {
                    // A und B familyName [und ...]
                    return (fullName[p0..^0], fullName[0..(p0 - 1)]);
                } else {
                    // familyName und ... A und B
                    var p3 = fullName.LastIndexOf(' ', p2 - 1);
                    return (fullName[0..p3], fullName[(p3 + 1)..^0]);
                }
            } else {
                return (familyName, fullName.Replace(familyName, "").Replace("  ", " ").Trim());
            }
        }

        public static IEnumerable<IEnumerable<T>> Permutate<T>(IEnumerable<T> input, IEnumerable<T>? forced = null) {
            HashSet<IEnumerable<T>> output = [];
            for (int i = 0; i < Math.Pow(2, input.Count()); i++) {
                List<T> t = [];
                for (int j = 0; j < 30; j++) {
                    var e = input.ElementAtOrDefault(j);
                    if (e != null && ((forced?.Contains(e) ?? false) || (i & (1 << j)) != 0)) {
                        t.Add(e);
                    }
                }
                output.Add(t);
            }
            return output.OrderByDescending(l => l.Count());
        }

        public static List<RawVaribute> GetVaributes(AppDbContext ctx, int year, bool onlyDelivered = true) {
            var varieties = ctx.WineVarieties.Select(v => new RawVaribute(v.SortId, "", null)).ToList();
            var delivered = ctx.DeliveryParts
               .Where(d => d.Year == year)
               .Select(d => new RawVaribute(d.SortId, d.AttrId ?? "", d.CultId ?? ""))
               .Distinct()
               .ToList();
            return [.. (onlyDelivered ? delivered : delivered.Union(varieties)).Order()];
        }

        public static List<Varibute> GetVaributeList(AppDbContext ctx, int year, bool onlyDelivered = true) {
            var varieties = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v);
            var attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a);
            var cultivations = ctx.WineCultivations.ToDictionary(c => c.CultId, c => c);
            return GetVaributes(ctx, year, onlyDelivered)
                .Select(s => new Varibute(s, varieties, attributes, cultivations))
                .ToList();
        }

        [LibraryImport("wininet.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static partial bool InternetGetConnectedState(out int description, int reservedValue);

        public static bool HasInternetConnectivity() {
            return InternetGetConnectedState(out var _, 0);
        }

        public static async Task<(string Version, string Url, long Size)?> GetLatestInstallerUrl(string url) {
            try {
                using var client = new HttpClient() {
                    Timeout = TimeSpan.FromSeconds(5),
                };
                var res = JsonNode.Parse(await client.GetStringAsync(url));
                var data = res!["data"]![0]!;
                return ((string)data["version"]!, (string)data["url"]!, (int)data["size"]!);
            } catch {
                return null;
            }
        }

        public static void CleanupTempFiles() {
            var dir = new DirectoryInfo(App.TempPath);
            foreach (var file in TempWildcards.SelectMany(dir.EnumerateFiles)) {
                file.Delete();
            }
        }

        public static string NormalizeFileName(string filename) {
            return InvalidFileNamePartsRegex.Replace(filename.Replace('/', '-'), "_");
        }

        public static async Task<SmtpClient?> GetSmtpClient() {
            if (App.Config.Smtp == null)
                return null;
            var (host, port, mode, username, password, _) = App.Config.Smtp.Value;
            var client = new SmtpClient();
            await client.ConnectAsync(host, port, mode == "starttls" ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
            await client.AuthenticateAsync(username, password);
            return client;
        }
    }
}