using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;

namespace Barotrauma
{
    [Obsolete("Use named tuples instead.")]
    public class Pair<T1, T2>
    {
        public T1 First { get; set; }
        public T2 Second { get; set; }

        public Pair(T1 first, T2 second)
        {
            First  = first;
            Second = second;
        }
    }

    static partial class ToolBox
    {
        public static bool IsProperFilenameCase(string filename)
        {
            //File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.
            //It also seems Path.GetFullPath may return a path with an incorrect case on Windows when the case of any of the game's
            //parent folders have been changed.
#if !DEBUG && !LINUX
            return true;
#endif

            CorrectFilenameCase(filename, out bool corrected);

            return !corrected;
        }

        public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "")
        {
            char[] delimiters = { '/', '\\' };
            string[] subDirs = filename.Split(delimiters);
            string originalFilename = filename;
            filename = "";
            corrected = false;

#if !WINDOWS
            if (File.Exists(originalFilename))
            {
                return originalFilename;
            }
#endif

            string startPath = directory ?? "";

            string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/');
            if (originalFilename.Replace('\\', '/').StartsWith(saveFolder))
            {
                //paths that lead to the save folder might have incorrect case,
                //mainly if they come from a filelist
                startPath = saveFolder.EndsWith('/') ? saveFolder : $"{saveFolder}/";
                filename = startPath;
                subDirs = subDirs.Skip(saveFolder.Split('/').Length).ToArray();
            }
            else if (Path.IsPathRooted(originalFilename))
            {
                #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing.
                return originalFilename; //assume that rooted paths have correct case since these are generated by the game
            }

            for (int i = 0; i < subDirs.Length; i++)
            {
                if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i]))
                {
                    break;
                }

                string subDir = subDirs[i].TrimEnd();
                string enumPath = Path.Combine(startPath, filename);

                if (string.IsNullOrWhiteSpace(filename))
                {
                    enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath;
                }
                
                string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray();

                if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal)))
                {
                    filename += subDir;
                }
                else
                {
                    string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray();
                    if (correctedPaths.Any())
                    {
                        corrected = true;
                        filename += correctedPaths.First();
                    }
                    else
                    {
                        //DebugConsole.ThrowError($"File \"{originalFilename}\" not found!");
                        corrected = false;
                        return originalFilename;
                    }
                }
                if (i < subDirs.Length - 1) { filename += "/"; }
            }

            return filename;
        }

        public static string RemoveInvalidFileNameChars(string fileName)
        {
            var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(new char[] {';'});
            foreach (char invalidChar in invalidChars)
            {
                fileName = fileName.Replace(invalidChar.ToString(), "");
            }
            return fileName;
        }

        private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex = 
            new System.Text.RegularExpressions.Regex(@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]");

        public static string RemoveBBCodeTags(string str)
        {
            if (string.IsNullOrEmpty(str)) { return str; }
            return removeBBCodeRegex.Replace(str, "");
        }

        public static string RandomSeed(int length)
        {
            var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            return new string(
                Enumerable.Repeat(chars, length)
                          .Select(s => s[Rand.Int(s.Length)])
                          .ToArray());
        }

        public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant());

        public static int StringToInt(string str)
        {
            str = str.Substring(0, Math.Min(str.Length, 32));

            str = str.PadLeft(4, 'a');

            byte[] asciiBytes = Encoding.ASCII.GetBytes(str);

            for (int i = 4; i < asciiBytes.Length; i++)
            {
                asciiBytes[i % 4] ^= asciiBytes[i];
            }

            return BitConverter.ToInt32(asciiBytes, 0);
        }

        /// <summary>
        /// a method for changing inputtypes with old names to the new ones to ensure backwards compatibility with older subs
        /// </summary>
        public static string ConvertInputType(string inputType)
        {
            if (inputType == "ActionHit" || inputType == "Action") return "Use";
            if (inputType == "SecondaryHit" || inputType == "Secondary") return "Aim";

            return inputType;
        }

        /// <summary>
        /// Convert a HSV value into a RGB value.
        /// </summary>
        /// <param name="hue">Value between 0 and 360</param>
        /// <param name="saturation">Value between 0 and 1</param>
        /// <param name="value">Value between 0 and 1</param>
        /// <see href="https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB">Reference</see>
        /// <returns></returns>
        public static Color HSVToRGB(float hue, float saturation, float value)
        {
            float c = value * saturation;

            float h = Math.Clamp(hue, 0, 360) / 60f;

            float x = c * (1 - Math.Abs(h % 2 - 1));

            float r = 0,
                  g = 0,
                  b = 0;

            if (0 <= h && h <= 1)     { r = c; g = x; b = 0; }
            else if (1 < h && h <= 2) { r = x; g = c; b = 0; }
            else if (2 < h && h <= 3) { r = 0; g = c; b = x; }
            else if (3 < h && h <= 4) { r = 0; g = x; b = c; }
            else if (4 < h && h <= 5) { r = x; g = 0; b = c; }
            else if (5 < h && h <= 6) { r = c; g = 0; b = x; }

            float m = value - c;

            return new Color(r + m, g + m, b + m);
        }

        /// <summary>
        /// Returns either a green [x] or a red [o]
        /// </summary>
        /// <param name="isFinished"></param>
        /// <param name="isRunning"></param>
        /// <returns></returns>
        public static string GetDebugSymbol(bool isFinished, bool isRunning = false)
        {
            return isRunning ? "[‖color:243,162,50‖x‖color:end‖]" : $"[‖color:{(isFinished ? "0,255,0‖x" : "255,0,0‖o")}‖color:end‖]";
        }

        /// <summary>
        /// Turn the object into a string and give it rich color based on the object type
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static string ColorizeObject(this object obj)
        {
            string color = obj switch
            {
                bool       b => b ? "80,250,123" : "255,85,85",
                string     _ => "241,250,140",
                Identifier _ => "241,250,140",
                int        _ => "189,147,249",
                float      _ => "189,147,249",
                double     _ => "189,147,249",
                null         => "255,85,85",
                _ => "139,233,253"
            };

            return obj is string || obj is Identifier
                ? $"‖color:{color}‖\"{obj}\"‖color:end‖" 
                : $"‖color:{color}‖{obj ?? "null"}‖color:end‖";
        }

        // Convert an RGB value into an HLS value.
        public static Vector3 RgbToHLS(Vector3 color)
        {
            double h, l, s;
            
            double double_r = color.X;
            double double_g = color.Y;
            double double_b = color.Z;

            // Get the maximum and minimum RGB components.
            double max = double_r;
            if (max < double_g) max = double_g;
            if (max < double_b) max = double_b;

            double min = double_r;
            if (min > double_g) min = double_g;
            if (min > double_b) min = double_b;

            double diff = max - min;
            l = (max + min) / 2;
            if (Math.Abs(diff) < 0.00001)
            {
                s = 0;
                h = 0;  // H is really undefined.
            }
            else
            {
                if (l <= 0.5) s = diff / (max + min);
                else s = diff / (2 - max - min);

                double r_dist = (max - double_r) / diff;
                double g_dist = (max - double_g) / diff;
                double b_dist = (max - double_b) / diff;

                if (double_r == max) h = b_dist - g_dist;
                else if (double_g == max) h = 2 + r_dist - b_dist;
                else h = 4 + g_dist - r_dist;

                h = h * 60;
                if (h < 0) h += 360;
            }

            return new Vector3((float)h, (float)l, (float)s);
        }

        /// <summary>
        /// Calculates the minimum number of single-character edits (i.e. insertions, deletions or substitutions) required to change one string into the other
        /// </summary>
        public static int LevenshteinDistance(string s, string t)
        {
            int n = s.Length;
            int m = t.Length;
            int[,] d = new int[n + 1, m + 1];

            if (n == 0 || m == 0) return 0;

            for (int i = 0; i <= n; d[i, 0] = i++) ;
            for (int j = 0; j <= m; d[0, j] = j++) ;

            for (int i = 1; i <= n; i++)
            {
                for (int j = 1; j <= m; j++)
                {
                    int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;

                    d[i, j] = Math.Min(
                        Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                        d[i - 1, j - 1] + cost);
                }
            }

            return d[n, m];
        }

        public static LocalizedString SecondsToReadableTime(float seconds)
        {
            int s = (int)(seconds % 60.0f);
            if (seconds < 60.0f)
            {
                return TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString());
            }

            int h = (int)(seconds / (60.0f * 60.0f));
            int m = (int)((seconds / 60.0f) % 60);

            LocalizedString text = "";
            if (h != 0) { text = TextManager.GetWithVariable("timeformathours", "[hours]", h.ToString()); }
            if (m != 0)
            {
                LocalizedString minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString());
                text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(" ", text, minutesText);
            }
            if (s != 0)
            {
                LocalizedString secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString());
                text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(" ", text, secondsText);
            }
            return text;
        }

        private static Dictionary<string, List<string>> cachedLines = new Dictionary<string, List<string>>();
        public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
        {
            List<string> lines;
            if (cachedLines.ContainsKey(filePath))
            {
                lines = cachedLines[filePath];
            }
            else
            {
                try
                {
                    lines = File.ReadAllLines(filePath).ToList();
                    cachedLines.Add(filePath, lines);
                    if (lines.Count == 0)
                    {
                        DebugConsole.ThrowError("File \"" + filePath + "\" is empty!");
                        return "";
                    }
                }
                catch (Exception e)
                {
                    DebugConsole.ThrowError("Couldn't open file \"" + filePath + "\"!", e);
                    return "";
                }
            }

            if (lines.Count == 0) return "";
            return lines[Rand.Range(0, lines.Count, randSync)];
        }

        /// <summary>
        /// Reads a number of bits from the buffer and inserts them to a new NetBuffer instance
        /// </summary>
        public static IReadMessage ExtractBits(this IReadMessage originalBuffer, int numberOfBits)
        {
            var buffer = new ReadWriteMessage();

            for (int i = 0; i < numberOfBits; i++)
            {
                bool bit = originalBuffer.ReadBoolean();
                buffer.WriteBoolean(bit);
            }
            buffer.BitPosition = 0;

            return buffer;
        }

        public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Rand.RandSync randSync)
        {
            return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync));
        }

        public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Random random)
        {
            List<T> objectList = objects.ToList();
            List<float> weights = objectList.Select(o => weightMethod(o)).ToList();
            return SelectWeightedRandom(objectList, weights, random);
        }

        public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Rand.RandSync randSync)
        {
            return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync));
        }

        public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Random random)
        {
            if (objects.Count == 0) { return default(T); }

            if (objects.Count != weights.Count)
            {
                DebugConsole.ThrowError("Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace());
                return objects[0];
            }

            float totalWeight = weights.Sum();

            float randomNum = (float)(random.NextDouble() * totalWeight);
            for (int i = 0; i < objects.Count; i++)
            {
                if (randomNum <= weights[i])
                {
                    return objects[i];
                }
                randomNum -= weights[i];
            }
            return default(T);
        }

        public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5)
            => StringToUInt32Hash(id.Value.ToLowerInvariant(), md5);

        public static UInt32 StringToUInt32Hash(string str, MD5 md5)
        {
            //calculate key based on MD5 hash instead of string.GetHashCode
            //to ensure consistent results across platforms
            byte[] inputBytes = Encoding.UTF8.GetBytes(str);
            byte[] hash = md5.ComputeHash(inputBytes);

            UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead?
            key |= (UInt32)(hash[hash.Length - 3] << 16);
            key |= (UInt32)(hash[hash.Length - 2] << 8);
            key |= (UInt32)(hash[hash.Length - 1]);

            return key;
        }

        /// <summary>
        /// Returns a new instance of the class with all properties and fields copied.
        /// </summary>
        public static T CreateCopy<T>(this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags);
        public static T CopyValuesTo<T>(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags);

        /// <summary>
        /// Copies the values of the source to the destination. May not work, if the source is of higher inheritance class than the destination. Does not work with virtual properties.
        /// </summary>
        public static T CopyValues<T>(T source, T destination, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public)
        {
            if (source == null)
            {
                throw new Exception("Failed to copy object. Source is null.");
            }
            if (destination == null)
            {
                throw new Exception("Failed to copy object. Destination is null.");
            }
            Type type = source.GetType();
            var properties = type.GetProperties(flags);
            foreach (var property in properties)
            {
                if (property.CanWrite)
                {
                    property.SetValue(destination, property.GetValue(source, null), null);
                }
            }
            var fields = type.GetFields(flags);
            foreach (var field in fields)
            {
                field.SetValue(destination, field.GetValue(source));
            }
            // Check that the fields match.Uncomment to apply the test, if in doubt.
            //if (fields.Any(f => { var value = f.GetValue(destination); return value == null || !value.Equals(f.GetValue(source)); }))
            //{
            //    throw new Exception("Failed to copy some of the fields.");
            //}
            return destination;
        }

        public static void SiftElement<T>(this List<T> list, int from, int to)
        {
            if (from < 0 || from >= list.Count) { throw new ArgumentException($"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); }
            if (to < 0 || to >= list.Count) { throw new ArgumentException($"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); }

            T elem = list[from];
            if (from > to)
            {
                for (int i = from; i > to; i--)
                {
                    list[i] = list[i - 1];
                }
                list[to] = elem;
            }
            else if (from < to)
            {
                for (int i = from; i < to; i++)
                {
                    list[i] = list[i + 1];
                }
                list[to] = elem;
            }
        }

        public static string ByteArrayToString(byte[] ba)
        {
            StringBuilder hex = new StringBuilder(ba.Length * 2);
            foreach (byte b in ba)
                hex.AppendFormat("{0:x2}", b);
            return hex.ToString();
        }

        public static string EscapeCharacters(string str)
        {
            return str.Replace("\\", "\\\\").Replace("\"", "\\\"");
        }

        public static string UnescapeCharacters(string str)
        {
            string retVal = "";
            for (int i = 0; i < str.Length; i++)
            {
                if (str[i] != '\\')
                {
                    retVal += str[i];
                }
                else if (i+1<str.Length)
                {
                    if (str[i+1] == '\\')
                    {
                        retVal += "\\";
                    }
                    else if (str[i+1] == '\"')
                    {
                        retVal += "\"";
                    }
                    i++;
                }
            }
            return retVal;
        }

        public static string[] SplitCommand(string command)
        {
            command = command.Trim();

            List<string> commands = new List<string>();
            int escape = 0;
            bool inQuotes = false;
            string piece = "";

            for (int i = 0; i < command.Length; i++)
            {
                if (command[i] == '\\')
                {
                    if (escape == 0) escape = 2;
                    else piece += '\\';
                }
                else if (command[i] == '"')
                {
                    if (escape == 0) inQuotes = !inQuotes;
                    else piece += '"';
                }
                else if (command[i] == ' ' && !inQuotes)
                {
                    if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
                    piece = "";
                }
                else if (escape == 0) piece += command[i];

                if (escape > 0) escape--;
            }

            if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece); //add final piece

            return commands.ToArray();
        }

        /// <summary>
        /// Cleans up a path by replacing backslashes with forward slashes, and
        /// optionally corrects the casing of the path. Recommended when serializing
        /// paths to a human-readable file to force case correction on all platforms.
        /// Also useful when working with paths to files that currently don't exist,
        /// i.e. case cannot be corrected.
        /// </summary>
        /// <param name="path">Path to clean up</param>
        /// <param name="correctFilenameCase">Should the case be corrected to match the filesystem?</param>
        /// <param name="directory">Directories that the path should be found in, not returned.</param>
        /// <returns>Path with corrected slashes, and corrected case if requested.</returns>
        public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "")
        {
            if (string.IsNullOrEmpty(path)) { return ""; }

            path = path
                .Replace('\\', '/');
            if (path.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
            {
                path = path.Substring("file:".Length);
            }
            while (path.IndexOf("//") >= 0)
            {
                path = path.Replace("//", "/");
            }

            if (correctFilenameCase)
            {
                string correctedPath = CorrectFilenameCase(path, out _, directory);
                if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; }
            }

            return path;
        }

        /// <summary>
        /// Cleans up a path by replacing backslashes with forward slashes, and
        /// corrects the casing of the path on non-Windows platforms. Recommended
        /// when loading a path from a file, to make sure that it is found on all
        /// platforms when attempting to open it.
        /// </summary>
        /// <param name="path">Path to clean up</param>
        /// <returns>Path with corrected slashes, and corrected case if required by the platform.</returns>
        public static string CleanUpPath(this string path)
        {
            return path.CleanUpPathCrossPlatform(
                correctFilenameCase:
#if WINDOWS
                    false
#else
                    true
#endif
                );
        }

        public static float GetEasing(TransitionMode easing, float t)
        {
            return easing switch
            {
                TransitionMode.Smooth => MathUtils.SmoothStep(t),
                TransitionMode.Smoother => MathUtils.SmootherStep(t),
                TransitionMode.EaseIn => MathUtils.EaseIn(t),
                TransitionMode.EaseOut => MathUtils.EaseOut(t),
                TransitionMode.Exponential => t * t,
                TransitionMode.Linear => t,
                _ => t,
            };
        }

        public static Rectangle GetWorldBounds(Point center, Point size)
        {
            Point halfSize = size.Divide(2);
            Point topLeft = new Point(center.X - halfSize.X, center.Y + halfSize.Y);
            return new Rectangle(topLeft, size);
        }

        public static Exception GetInnermost(this Exception e)
        {
            while (e.InnerException != null) { e = e.InnerException; }

            return e;
        }

        public static void ThrowIfNull<T>([NotNull] T o)
        {
            if (o is null) { throw new ArgumentNullException(); }
        }

        public static string GetFormattedPercentage(float v)
        {
            return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value;
        }

        private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create('%', '+', '％');

        /// <summary>
        /// Extends % and + characters to color tags in talent name tooltips to make them look nicer.
        /// This obviously does not work in languages like French where a non breaking space is used
        /// so it's just a a bit extra for the languages it works with.
        /// </summary>
        /// <param name="original"></param>
        /// <returns></returns>
        public static string ExtendColorToPercentageSigns(string original)
        {
            const string colorEnd = "‖color:end‖",
                         colorStart = "‖color:";

            const char definitionIndicator = '‖';

            char[] chars = original.ToCharArray();

            for (int i = 0; i < chars.Length; i++)
            {
                if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; }

                // look behind
                if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator)
                {
                    int offset = colorEnd.Length;

                    if (MatchesSequence(i - offset, colorEnd, chars))
                    {
                        // push the color end tag forwards until the character is within the tag
                        char prev = currentChar;
                        for (int k = i - offset; k <= i; k++)
                        {
                            if (!TryGetAt(k, chars, out c)) { continue; }

                            chars[k] = prev;
                            prev = c;
                        }
                        continue;
                    }
                }

                // look ahead
                if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator)
                {
                    if (!MatchesSequence(i + 1, colorStart, chars)) { continue; }

                    int offset = FindNextDefinitionOffset(i, colorStart.Length, chars);

                    // we probably reached the end of the string
                    if (offset > chars.Length) { continue; }

                    // push the color start tag back until the character is within the tag
                    char prev = currentChar;
                    for (int k = i + offset; k >= i; k--)
                    {
                        if (!TryGetAt(k, chars, out c)) { continue; }

                        chars[k] = prev;
                        prev = c;
                    }

                    // skip needlessly checking this section again since we already know what's ahead
                    i += offset;
                }
            }

            static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars)
            {
                int offset = initialOffset;
                while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; }
                return offset;
            }

            static bool MatchesSequence(int index, string sequence, char[] chars)
            {
                for (int i = 0; i < sequence.Length; i++)
                {
                    if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; }
                }

                return true;
            }

            static bool TryGetAt(int i, char[] chars, out char c)
            {
                if (i >= 0 && i < chars.Length)
                {
                    c = chars[i];
                    return true;
                }

                c = default;
                return false;
            }

            return new string(chars);
        }

        public static bool StatIdentifierMatches(Identifier original, Identifier match)
        {
            if (original == match) { return true; }
            return Matches(original, match) || Matches(match, original);

            static bool Matches(Identifier a, Identifier b)
            {
                for (int i = 0; i < b.Value.Length; i++)
                {
                    if (i >= a.Value.Length) { return b[i] is '~'; }
                    if (!CharEquals(a[i], b[i])) { return false; }
                }
                return false;
            }

            static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b);
        }

        public static bool EquivalentTo(this IPEndPoint self, IPEndPoint other)
            => self.Address.EquivalentTo(other.Address) && self.Port == other.Port;

        public static bool EquivalentTo(this IPAddress self, IPAddress other)
        {
            if (self.IsIPv4MappedToIPv6) { self = self.MapToIPv4(); }
            if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); }
            return self.Equals(other);
        }
    }
}
