/* Copyright (c) 2013 Max Hauser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.Globalization; using System.Text; #if !NETSTANDARD using System.Runtime.Serialization; using System.Security.Permissions; #endif using System.Text.RegularExpressions; namespace VRCMelonAssistant.Libs { /// /// A semantic version implementation. /// Conforms with v2.0.0 of http://semver.org /// #if NETSTANDARD public sealed class SemVersion : IComparable, IComparable #else [Serializable] public sealed class SemVersion : IComparable, IComparable, ISerializable #endif { private static readonly Regex ParseEx = new Regex(@"^(?\d+)" + @"(?>\.(?\d+))?" + @"(?>\.(?\d+))?" + @"(?>\-(?
[0-9A-Za-z\-\.]+))?" +
                @"(?>\+(?[0-9A-Za-z\-\.]+))?$",
#if NETSTANDARD
                RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture,
#else
                RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture,
#endif
                TimeSpan.FromSeconds(0.5));

#if !NETSTANDARD
#pragma warning disable CA1801 // Parameter unused
        /// 
        /// Deserialize a .
        /// 
        /// The  parameter is null.
        private SemVersion(SerializationInfo info, StreamingContext context)
#pragma warning restore CA1801 // Parameter unused
        {
            if (info == null) throw new ArgumentNullException(nameof(info));
            var semVersion = Parse(info.GetString("SemVersion"));
            Major = semVersion.Major;
            Minor = semVersion.Minor;
            Patch = semVersion.Patch;
            Prerelease = semVersion.Prerelease;
            Build = semVersion.Build;
        }
#endif

        /// 
        /// Constructs a new instance of the  class.
        /// 
        /// The major version.
        /// The minor version.
        /// The patch version.
        /// The prerelease version (e.g. "alpha").
        /// The build metadata (e.g. "nightly.232").
        public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = "", string build = "")
        {
            Major = major;
            Minor = minor;
            Patch = patch;

            Prerelease = prerelease ?? "";
            Build = build ?? "";
        }

        /// 
        /// Constructs a new instance of the  class from
        /// a .
        /// 
        /// The  that is used to initialize
        /// the Major, Minor, Patch and Build.
        /// A  with the same Major and Minor version.
        /// The Patch version will be the fourth part of the version number. The
        /// build meta data will contain the third part of the version number if
        /// it is greater than zero.
        public SemVersion(Version version)
        {
            if (version == null)
                throw new ArgumentNullException(nameof(version));

            Major = version.Major;
            Minor = version.Minor;

            if (version.Revision >= 0)
                Patch = version.Revision;

            Prerelease = "";

            Build = version.Build > 0 ? version.Build.ToString(CultureInfo.InvariantCulture) : "";
        }

        /// 
        /// Converts the string representation of a semantic version to its  equivalent.
        /// 
        /// The version string.
        /// If set to  minor and patch version are required,
        /// otherwise they are optional.
        /// The  object.
        /// The  is .
        /// The  has an invalid format.
        /// The  is missing Minor or Patch versions and  is .
        /// The Major, Minor, or Patch versions are larger than int.MaxValue.
        public static SemVersion Parse(string version, bool strict = false)
        {
            var match = ParseEx.Match(version);
            if (!match.Success)
                throw new ArgumentException($"Invalid version '{version}'.", nameof(version));

            var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);

            var minorMatch = match.Groups["minor"];
            int minor = 0;
            if (minorMatch.Success)
                minor = int.Parse(minorMatch.Value, CultureInfo.InvariantCulture);
            else if (strict)
                throw new InvalidOperationException("Invalid version (no minor version given in strict mode)");

            var patchMatch = match.Groups["patch"];
            int patch = 0;
            if (patchMatch.Success)
                patch = int.Parse(patchMatch.Value, CultureInfo.InvariantCulture);
            else if (strict)
                throw new InvalidOperationException("Invalid version (no patch version given in strict mode)");

            var prerelease = match.Groups["pre"].Value;
            var build = match.Groups["build"].Value;

            return new SemVersion(major, minor, patch, prerelease, build);
        }

        /// 
        /// Converts the string representation of a semantic version to its 
        /// equivalent and returns a value that indicates whether the conversion succeeded.
        /// 
        /// The version string.
        /// When the method returns, contains a  instance equivalent
        /// to the version string passed in, if the version string was valid, or  if the
        /// version string was not valid.
        /// If set to  minor and patch version are required,
        /// otherwise they are optional.
        ///  when a invalid version string is passed, otherwise .
        public static bool TryParse(string version, out SemVersion semver, bool strict = false)
        {
            semver = null;
            if (version is null) return false;

            var match = ParseEx.Match(version);
            if (!match.Success) return false;

            if (!int.TryParse(match.Groups["major"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var major))
                return false;

            var minorMatch = match.Groups["minor"];
            int minor = 0;
            if (minorMatch.Success)
            {
                if (!int.TryParse(minorMatch.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out minor))
                    return false;
            }
            else if (strict) return false;

            var patchMatch = match.Groups["patch"];
            int patch = 0;
            if (patchMatch.Success)
            {
                if (!int.TryParse(patchMatch.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out patch))
                    return false;
            }
            else if (strict) return false;

            var prerelease = match.Groups["pre"].Value;
            var build = match.Groups["build"].Value;

            semver = new SemVersion(major, minor, patch, prerelease, build);
            return true;
        }

        /// 
        /// Checks whether two semantic versions are equal.
        /// 
        /// The first version to compare.
        /// The second version to compare.
        ///  if the two values are equal, otherwise .
        public static bool Equals(SemVersion versionA, SemVersion versionB)
        {
            if (ReferenceEquals(versionA, versionB)) return true;
            if (versionA is null || versionB is null) return false;
            return versionA.Equals(versionB);
        }

        /// 
        /// Compares the specified versions.
        /// 
        /// The first version to compare.
        /// The second version to compare.
        /// A signed number indicating the relative values of  and .
        public static int Compare(SemVersion versionA, SemVersion versionB)
        {
            if (ReferenceEquals(versionA, versionB)) return 0;
            if (versionA is null) return -1;
            if (versionB is null) return 1;
            return versionA.CompareTo(versionB);
        }

        /// 
        /// Make a copy of the current instance with changed properties.
        /// 
        /// The value to replace the major version or  to leave it unchanged.
        /// The value to replace the minor version or  to leave it unchanged.
        /// The value to replace the patch version or  to leave it unchanged.
        /// The value to replace the prerelease version or  to leave it unchanged.
        /// The value to replace the build metadata or  to leave it unchanged.
        /// The new version object.
        /// 
        /// The change method is intended to be called using named argument syntax, passing only
        /// those fields to be changed.
        /// 
        /// 
        /// To change only the patch version:
        /// version.Change(patch: 4)
        /// 
        public SemVersion Change(int? major = null, int? minor = null, int? patch = null,
            string prerelease = null, string build = null)
        {
            return new SemVersion(
                major ?? Major,
                minor ?? Minor,
                patch ?? Patch,
                prerelease ?? Prerelease,
                build ?? Build);
        }

        /// 
        /// Gets the major version.
        /// 
        /// 
        /// The major version.
        /// 
        public int Major { get; }

        /// 
        /// Gets the minor version.
        /// 
        /// 
        /// The minor version.
        /// 
        public int Minor { get; }

        /// 
        /// Gets the patch version.
        /// 
        /// 
        /// The patch version.
        /// 
        public int Patch { get; }

        /// 
        /// Gets the prerelease version.
        /// 
        /// 
        /// The prerelease version. Empty string if this is a release version.
        /// 
        public string Prerelease { get; }

        /// 
        /// Gets the build metadata.
        /// 
        /// 
        /// The build metadata. Empty string if there is no build metadata.
        /// 
        public string Build { get; }

        /// 
        /// Returns the  equivalent of this version.
        /// 
        /// 
        /// The  equivalent of this version.
        /// 
        public override string ToString()
        {
            // Assume all separators ("..-+"), at most 2 extra chars
            var estimatedLength = 4 + Major.Digits() + Minor.Digits() + Patch.Digits()
                                  + Prerelease.Length + Build.Length;
            var version = new StringBuilder(estimatedLength);
            version.Append(Major);
            version.Append('.');
            version.Append(Minor);
            version.Append('.');
            version.Append(Patch);
            if (Prerelease.Length > 0)
            {
                version.Append('-');
                version.Append(Prerelease);
            }
            if (Build.Length > 0)
            {
                version.Append('+');
                version.Append(Build);
            }
            return version.ToString();
        }

        /// 
        /// Compares the current instance with another object of the same type and returns an integer that indicates
        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
        /// other object.
        /// 
        /// An object to compare with this instance.
        /// 
        /// A value that indicates the relative order of the objects being compared.
        /// The return value has these meanings:
        ///  Less than zero: This instance precedes  in the sort order.
        ///  Zero: This instance occurs in the same position in the sort order as .
        ///  Greater than zero: This instance follows  in the sort order.
        /// 
        /// The  is not a .
        public int CompareTo(object obj)
        {
            return CompareTo((SemVersion)obj);
        }

        /// 
        /// Compares the current instance with another object of the same type and returns an integer that indicates
        /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the
        /// other object.
        /// 
        /// An object to compare with this instance.
        /// 
        /// A value that indicates the relative order of the objects being compared.
        /// The return value has these meanings:
        ///  Less than zero: This instance precedes  in the sort order.
        ///  Zero: This instance occurs in the same position in the sort order as .
        ///  Greater than zero: This instance follows  in the sort order.
        /// 
        public int CompareTo(SemVersion other)
        {
            var r = CompareByPrecedence(other);
            if (r != 0) return r;

#pragma warning disable CA1062 // Validate arguments of public methods
            // If other is null, CompareByPrecedence() returns 1
            return CompareComponent(Build, other.Build);
#pragma warning restore CA1062 // Validate arguments of public methods
        }

        /// 
        /// Returns whether two semantic versions have the same precedence. Versions
        /// that differ only by build metadata have the same precedence.
        /// 
        /// The semantic version to compare to.
        ///  if the version precedences are equal.
        public bool PrecedenceMatches(SemVersion other)
        {
            return CompareByPrecedence(other) == 0;
        }

        /// 
        /// Compares two semantic versions by precedence as defined in the SemVer spec. Versions
        /// that differ only by build metadata have the same precedence.
        /// 
        /// The semantic version.
        /// 
        /// A value that indicates the relative order of the objects being compared.
        /// The return value has these meanings:
        ///  Less than zero: This instance precedes  in the sort order.
        ///  Zero: This instance occurs in the same position in the sort order as .
        ///  Greater than zero: This instance follows  in the sort order.
        /// 
        public int CompareByPrecedence(SemVersion other)
        {
            if (other is null)
                return 1;

            var r = Major.CompareTo(other.Major);
            if (r != 0) return r;

            r = Minor.CompareTo(other.Minor);
            if (r != 0) return r;

            r = Patch.CompareTo(other.Patch);
            if (r != 0) return r;

            return CompareComponent(Prerelease, other.Prerelease, true);
        }

        private static int CompareComponent(string a, string b, bool nonemptyIsLower = false)
        {
            var aEmpty = string.IsNullOrEmpty(a);
            var bEmpty = string.IsNullOrEmpty(b);
            if (aEmpty && bEmpty)
                return 0;

            if (aEmpty)
                return nonemptyIsLower ? 1 : -1;
            if (bEmpty)
                return nonemptyIsLower ? -1 : 1;

            var aComps = a.Split('.');
            var bComps = b.Split('.');

            var minLen = Math.Min(aComps.Length, bComps.Length);
            for (int i = 0; i < minLen; i++)
            {
                var ac = aComps[i];
                var bc = bComps[i];
                var aIsNum = int.TryParse(ac, out var aNum);
                var bIsNum = int.TryParse(bc, out var bNum);
                int r;
                if (aIsNum && bIsNum)
                {
                    r = aNum.CompareTo(bNum);
                    if (r != 0) return r;
                }
                else
                {
                    if (aIsNum)
                        return -1;
                    if (bIsNum)
                        return 1;
                    r = string.CompareOrdinal(ac, bc);
                    if (r != 0)
                        return r;
                }
            }

            return aComps.Length.CompareTo(bComps.Length);
        }

        /// 
        /// Determines whether the specified  is equal to this instance.
        /// 
        /// The  to compare with this instance.
        /// 
        ///    if the specified  is equal to this instance, otherwise .
        /// 
        /// The  is not a .
        public override bool Equals(object obj)
        {
            if (obj is null)
                return false;

            if (ReferenceEquals(this, obj))
                return true;

            var other = (SemVersion)obj;

            return Major == other.Major
                && Minor == other.Minor
                && Patch == other.Patch
                && string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal)
                && string.Equals(Build, other.Build, StringComparison.Ordinal);
        }

        /// 
        /// Returns a hash code for this instance.
        /// 
        /// 
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// 
        public override int GetHashCode()
        {
            unchecked
            {
                // TODO verify this. Some versions start result = 17. Some use 37 instead of 31
                int result = Major.GetHashCode();
                result = result * 31 + Minor.GetHashCode();
                result = result * 31 + Patch.GetHashCode();
                result = result * 31 + Prerelease.GetHashCode();
                result = result * 31 + Build.GetHashCode();
                return result;
            }
        }

#if !NETSTANDARD
        /// 
        /// Populates a  with the data needed to serialize the target object.
        /// 
        /// The  to populate with data.
        /// The destination (see ) for this serialization.
        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null) throw new ArgumentNullException(nameof(info));
            info.AddValue("SemVersion", ToString());
        }
#endif

#pragma warning disable CA2225 // Operator overloads have named alternates
        /// 
        /// Implicit conversion from  to .
        /// 
        /// The semantic version.
        /// The  object.
        /// The  is .
        /// The version number has an invalid format.
        /// The Major, Minor, or Patch versions are larger than int.MaxValue.
        public static implicit operator SemVersion(string version)
#pragma warning restore CA2225 // Operator overloads have named alternates
        {
            return Parse(version);
        }

        /// 
        /// Compares two semantic versions for equality.
        /// 
        /// The left value.
        /// The right value.
        /// If left is equal to right , otherwise .
        public static bool operator ==(SemVersion left, SemVersion right)
        {
            return Equals(left, right);
        }

        /// 
        /// Compares two semantic versions for inequality.
        /// 
        /// The left value.
        /// The right value.
        /// If left is not equal to right , otherwise .
        public static bool operator !=(SemVersion left, SemVersion right)
        {
            return !Equals(left, right);
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is greater than right , otherwise .
        public static bool operator >(SemVersion left, SemVersion right)
        {
            return Compare(left, right) > 0;
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is greater than or equal to right , otherwise .
        public static bool operator >=(SemVersion left, SemVersion right)
        {
            return Equals(left, right) || Compare(left, right) > 0;
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is less than right , otherwise .
        public static bool operator <(SemVersion left, SemVersion right)
        {
            return Compare(left, right) < 0;
        }

        /// 
        /// Compares two semantic versions.
        /// 
        /// The left value.
        /// The right value.
        /// If left is less than or equal to right , otherwise .
        public static bool operator <=(SemVersion left, SemVersion right)
        {
            return Equals(left, right) || Compare(left, right) < 0;
        }
    }
}