using System;
using System.Linq;
using Volpe.Cafe.Utils;

namespace Volpe.Cafe
{
    /// <summary>
    /// Provides an object for converting model year values to the <see cref="MinYear"/>-based indexes and vice-versa.
    /// </summary>
    [Serializable]
    public class ModelYear : IComparable, IComparable<ModelYear>
    {
        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelYear"/> class using the specified year value.
        /// </summary>
        /// <param name="year">The model year value to assign to the instance.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The specified model year is less than <see cref="MinYear"/>.
        /// </exception>
        ModelYear(int year)
        {
            if (year < MinYear) { throw new ArgumentOutOfRangeException("year", year, ModelYearTooSmall); }
            this.Year = year;
            this.Index = year - MinYear;
        }

        #endregion

        #region <<< Variables >>>

        /// <summary>Represents the minimum allowable model year value.</summary>
        public const int MinYear = 2000;

        /// <summary>Represents an array of predefined model years, from MY-2000 to MY-2100.</summary>
        public static readonly ModelYear[] ModelYears =
            (from year in Enumerable.Range(MinYear, 101)
             select new ModelYear(year)).ToArray();

        // ERROR strings
        const string ObjectIsNotModelYearOrNumeric = "The specified object is not a ModelYear instance or a numeric value.";
        const string ModelYearTooSmall = "The value specified for model year is too small.";

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets the <see cref="MinYear"/>-based index of the <see cref="ModelYear"/> instance.</summary>
        public int Index { get; private set; }
        /// <summary>Gets the actual year of the <see cref="ModelYear"/> instance.</summary>
        public int Year { get; private set; }

        #endregion

        #region <<< Methods >>>

        #region < Overloaded Operators >

        /// <summary>
        /// Determines whether the specified two values are equal.
        /// </summary>
        /// <param name="value1">The first value to compare.</param>
        /// <param name="value2">The second value to compare.</param>
        /// <returns>true, if the two values are equal; false, otherwise.</returns>
        public static bool operator ==(ModelYear value1, ModelYear value2)
        {
            return Equals(value1, value2);
        }
        /// <summary>
        /// Determines whether the specified two values are not equal.
        /// </summary>
        /// <param name="value1">The first value to compare.</param>
        /// <param name="value2">The second value to compare.</param>
        /// <returns>true, if the two values are not equal; false, otherwise.</returns>
        public static bool operator !=(ModelYear value1, ModelYear value2)
        {
            return !Equals(value1, value2);
        }

        /// <summary>
        /// Defines an implicit conversion of an <see cref="int"/> value to a <see cref="ModelYear"/> equivalent.
        /// </summary>
        public static explicit operator ModelYear(int value)
        {
            return New(value);
        }
        /// <summary>
        /// Defines an explicit conversion of a <see cref="ModelYear"/> value to an <see cref="int"/> equivalent.
        /// </summary>
        public static implicit operator int(ModelYear value)
        {
            return value.Year;
        }

        #endregion

        /// <summary>
        /// Returns an instance of the <see cref="ModelYear"/> class using the specified year value.
        /// </summary>
        /// <param name="year">The model year value to assign to the instance.</param>
        /// <returns>A <see cref="ModelYear"/> instance for the specified year value.</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The specified model year is less than <see cref="MinYear"/>.
        /// </exception>
        public static ModelYear New(int year)
        {
            return NewFromIndex(year - MinYear);
        }

        /// <summary>
        /// Returns an instance of the <see cref="ModelYear"/> class using the specified index value.
        /// </summary>
        /// <param name="index">The index of the model year to assign to the instance.</param>
        /// <returns>A <see cref="ModelYear"/> instance for the specified index value.</returns>
        /// <exception cref="ArgumentOutOfRangeException">The specified index is less than 0.</exception>
        public static ModelYear NewFromIndex(int index)
        {
            return (index < ModelYears.Length) ? ModelYears[index] : new ModelYear(index + MinYear);
        }

        /// <summary>
        /// Determines whether the specified model year is valid, that is, if it is greater than or equal to
        /// <see cref="MinYear"/>.
        /// </summary>
        /// <param name="year">The model year to test for validity.</param>
        /// <returns>true, if year is valid; false, otherwise.</returns>
        public static bool IsValid(int year)
        {
            return (year >= MinYear);
        }

        /// <summary>
        /// Returns the <see cref="MinYear"/>-based index of the given model year.
        /// </summary>
        /// <param name="year">The model year to convert.</param>
        /// <returns>The <see cref="MinYear"/>-based index of the given model year.</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The specified year is less than <see cref="MinYear"/>.
        /// </exception>
        public static int ToIndex(int year)
        {
            if (year < MinYear)
            {
                throw new ArgumentOutOfRangeException(nameof(year), year, ModelYearTooSmall);
            }
            return year - MinYear;
        }

        /// <summary>
        /// Returns the string representation of the current <see cref="ModelYear"/> instance.
        /// </summary>
        /// <returns>The string representation of the current <see cref="ModelYear"/> instance.</returns>
        public override string ToString()
        {
            return this.Year.ToString();
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        /// <returns>A 32-bit signed integer hash code.</returns>
        public override int GetHashCode()
        {
            return this.Year.GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified <see cref="object"/> is equal to the current instance.
        /// </summary>
        /// <param name="obj">The <see cref="object"/> to compare with the current instance.</param>
        /// <returns>
        /// true, if the specified <see cref="object"/> is equal to the current instance; false, otherwise.
        /// </returns>
        public override bool Equals(object obj)
        {
            return (obj is ModelYear) ? this.Equals((ModelYear)obj) : base.Equals(obj);
        }
        /// <summary>
        /// Determines whether the specified value is equal to the current instance.
        /// </summary>
        /// <param name="value">The value to compare with the current instance.</param>
        /// <returns>true, if the specified value is equal to the current instance; false, otherwise.</returns>
        public bool Equals(ModelYear value)
        {
            return Equals(this, value);
        }
        /// <summary>
        /// Determines whether the specified value is equal to the current instance.
        /// </summary>
        /// <param name="value">The value to compare with the current instance.</param>
        /// <returns>true, if the specified value is equal to the current instance; false, otherwise.</returns>
        public bool Equals(int value)
        {
            return Equals(this, New(value));
        }
        /// <summary>
        /// Determines whether the specified two values are equal.
        /// </summary>
        /// <param name="value1">The first value to compare.</param>
        /// <param name="value2">The second value to compare.</param>
        /// <returns>true, if the specified two values are equal; false, otherwise.</returns>
        public static bool Equals(ModelYear value1, ModelYear value2)
        {
            return (value1 is null && value2 is null) ? true
                 : (value1 is null || value2 is null) ? false
                 : value1.Year == value2.Year;
        }

        int IComparable.CompareTo(object value)
        {
            return (value == null) ? 1
                 : (value is ModelYear) ? Compare(this, (ModelYear)value)
                 : (value.IsNumeric()) ? Compare(this, New(value.GetInt32()))
                 : throw new ArgumentException(ObjectIsNotModelYearOrNumeric);
        }
        int IComparable<ModelYear>.CompareTo(ModelYear value)
        {
            return Compare(this, value);
        }

        static int Compare(ModelYear value1, ModelYear value2)
        {
            return value1.Year.CompareTo(value2.Year);
        }

        #endregion
    }
}
