// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace NuGet.Versioning
{
    /// <summary>
    /// A base version range that handles ranges only and not any of the preferred version logic.
    /// </summary>
    public abstract class VersionRangeBase : IEquatable<VersionRangeBase>
    {
        private readonly bool _includeMinVersion;
        private readonly bool _includeMaxVersion;
        private readonly NuGetVersion _minVersion;
        private readonly NuGetVersion _maxVersion;

        /// <summary>
        /// Creates a VersionRange with the given min and max.
        /// </summary>
        /// <param name="minVersion">Lower bound of the version range.</param>
        /// <param name="includeMinVersion">True if minVersion satisfies the condition.</param>
        /// <param name="maxVersion">Upper bound of the version range.</param>
        /// <param name="includeMaxVersion">True if maxVersion satisfies the condition.</param>
        public VersionRangeBase(
            NuGetVersion minVersion = null,
            bool includeMinVersion = true,
            NuGetVersion maxVersion = null,
            bool includeMaxVersion = false)
        {
            _minVersion = minVersion;
            _maxVersion = maxVersion;
            _includeMinVersion = includeMinVersion;
            _includeMaxVersion = includeMaxVersion;
        }

        /// <summary>
        /// True if MinVersion exists;
        /// </summary>
        public bool HasLowerBound
        {
            get { return _minVersion != null; }
        }

        /// <summary>
        /// True if MaxVersion exists.
        /// </summary>
        public bool HasUpperBound
        {
            get { return _maxVersion != null; }
        }

        /// <summary>
        /// True if both MinVersion and MaxVersion exist.
        /// </summary>
        public bool HasLowerAndUpperBounds
        {
            get { return HasLowerBound && HasUpperBound; }
        }

        /// <summary>
        /// True if MinVersion exists and is included in the range.
        /// </summary>
        public bool IsMinInclusive
        {
            get { return HasLowerBound && _includeMinVersion; }
        }

        /// <summary>
        /// True if MaxVersion exists and is included in the range.
        /// </summary>
        public bool IsMaxInclusive
        {
            get { return HasUpperBound && _includeMaxVersion; }
        }

        /// <summary>
        /// Maximum version allowed by this range.
        /// </summary>
        public NuGetVersion MaxVersion
        {
            get { return _maxVersion; }
        }

        /// <summary>
        /// Minimum version allowed by this range.
        /// </summary>
        public NuGetVersion MinVersion
        {
            get { return _minVersion; }
        }

        /// <summary>
        /// Determines if an NuGetVersion meets the requirements.
        /// </summary>
        /// <param name="version">SemVer to compare</param>
        /// <returns>True if the given version meets the version requirements.</returns>
        public bool Satisfies(NuGetVersion version)
        {
            // ignore metadata by default when finding a range.
            return Satisfies(version, VersionComparer.VersionRelease);
        }

        /// <summary>
        /// Determines if an NuGetVersion meets the requirements using the given mode.
        /// </summary>
        /// <param name="version">SemVer to compare</param>
        /// <param name="versionComparison">VersionComparison mode used to determine the version range.</param>
        /// <returns>True if the given version meets the version requirements.</returns>
        public bool Satisfies(NuGetVersion version, VersionComparison versionComparison)
        {
            return Satisfies(version, new VersionComparer(versionComparison));
        }

        /// <summary>
        /// Determines if an NuGetVersion meets the requirements using the version comparer.
        /// </summary>
        /// <param name="version">SemVer to compare.</param>
        /// <param name="comparer">Version comparer used to determine if the version criteria is met.</param>
        /// <returns>True if the given version meets the version requirements.</returns>
        public bool Satisfies(NuGetVersion version, IVersionComparer comparer)
        {
            if (version == null)
            {
                throw new ArgumentNullException(nameof(version));
            }

            // Determine if version is in the given range using the comparer.
            var condition = true;
            if (HasLowerBound)
            {
                if (IsMinInclusive)
                {
                    condition &= comparer.Compare(MinVersion, version) <= 0;
                }
                else
                {
                    condition &= comparer.Compare(MinVersion, version) < 0;
                }
            }

            if (HasUpperBound)
            {
                if (IsMaxInclusive)
                {
                    condition &= comparer.Compare(MaxVersion, version) >= 0;
                }
                else
                {
                    condition &= comparer.Compare(MaxVersion, version) > 0;
                }
            }

            return condition;
        }

        /// <summary>
        /// Compares the object as a VersionRange with the default comparer
        /// </summary>
        public override bool Equals(object obj)
        {
            var range = obj as VersionRangeBase;

            if (range != null)
            {
                return VersionRangeComparer.Default.Equals(this, range);
            }

            return false;
        }

        /// <summary>
        /// Returns the hash code using the default comparer.
        /// </summary>
        public override int GetHashCode()
        {
            return VersionRangeComparer.Default.GetHashCode(this);
        }

        /// <summary>
        /// Default compare
        /// </summary>
        public bool Equals(VersionRangeBase other)
        {
            return Equals(other, VersionRangeComparer.Default);
        }

        /// <summary>
        /// Use the VersionRangeComparer for equality checks
        /// </summary>
        public bool Equals(VersionRangeBase other, IVersionRangeComparer comparer)
        {
            if (comparer == null)
            {
                throw new ArgumentNullException(nameof(comparer));
            }

            return comparer.Equals(this, other);
        }

        /// <summary>
        /// Use a specific VersionComparison for comparison
        /// </summary>
        public bool Equals(VersionRangeBase other, VersionComparison versionComparison)
        {
            IVersionRangeComparer comparer = new VersionRangeComparer(versionComparison);
            return Equals(other, comparer);
        }

        /// <summary>
        /// Use a specific IVersionComparer for comparison
        /// </summary>
        public bool Equals(VersionRangeBase other, IVersionComparer versionComparer)
        {
            IVersionRangeComparer comparer = new VersionRangeComparer(versionComparer);
            return Equals(other, comparer);
        }

        /// <summary>
        /// SubSet check
        /// </summary>
        public bool IsSubSetOrEqualTo(VersionRangeBase possibleSuperSet)
        {
            return IsSubSetOrEqualTo(possibleSuperSet, VersionComparer.Default);
        }

        /// <summary>
        /// SubSet check
        /// </summary>
        public bool IsSubSetOrEqualTo(VersionRangeBase possibleSuperSet, IVersionComparer comparer)
        {
            var rangeComparer = new VersionRangeComparer(comparer);

            var possibleSubSet = this;
            var target = possibleSuperSet;

            if (rangeComparer.Equals(possibleSubSet, VersionRange.None))
            {
                return true;
            }

            if (rangeComparer.Equals(target, VersionRange.None))
            {
                return false;
            }

            if (target == null)
            {
                target = VersionRange.All;
            }

            if (possibleSubSet == null)
            {
                possibleSubSet = VersionRange.All;
            }

            var result = true;

            if (possibleSubSet.HasLowerBound)
            {
                // normal check
                if (!target.Satisfies(possibleSubSet.MinVersion))
                {
                    // it's possible we didn't need that version, do a special non inclusive check
                    if (!possibleSubSet.IsMinInclusive
                        && !target.IsMinInclusive)
                    {
                        result &= comparer.Equals(target.MinVersion, possibleSubSet.MinVersion);
                    }
                    else
                    {
                        result = false;
                    }
                }
            }
            else
            {
                result &= !target.HasLowerBound;
            }

            if (possibleSubSet.HasUpperBound)
            {
                // normal check
                if (!target.Satisfies(possibleSubSet.MaxVersion))
                {
                    // it's possible we didn't need that version, do a special non inclusive check
                    if (!possibleSubSet.IsMaxInclusive
                        && !target.IsMaxInclusive)
                    {
                        result &= comparer.Equals(target.MaxVersion, possibleSubSet.MaxVersion);
                    }
                    else
                    {
                        result = false;
                    }
                }
            }
            else
            {
                result &= !target.HasUpperBound;
            }

            return result;
        }

        /// <summary>
        /// Infer if the range should allow prerelease versions based on if the lower or upper bounds 
        /// contain prerelease labels.
        /// </summary>
        protected bool HasPrereleaseBounds
        {
            get
            {
                return IsPrerelease(_minVersion) == true
                    || IsPrerelease(_maxVersion) == true;
            }
        }

        private static bool? IsPrerelease(SemanticVersion version)
        {
            bool? b = null;

            if (version != null)
            {
                b = version.IsPrerelease;
            }

            return b;
        }
    }
}
