using System;
using System.Linq;
using System.Runtime.InteropServices;
using Volpe.Cafe.TechKeys;
using Volpe.Cafe.Utils;

namespace Volpe.Cafe
{
    /// <summary>
    /// Defines a 128-bit key of model years. This value supports model years in the range from 2000 to 2127.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    [Serializable]
    public struct ModelYearKey : IComparable, IComparable<ModelYearKey>
    {
        #region <<< Variables >>>

        //----- const/static variables -----
        /// <summary>Represents the minimum allowable model year value supported for key generation.</summary>
        public const int MinYear = 2000;
        /// <summary>Represents the maximum allowable model year value supported for key generation.</summary>
        public const int MaxYear = 2127;

        /// <summary>Defines the number of supported keys.</summary>
        public const int KeyCount = MaxYear - MinYear + 1;

        /// <summary>
        /// Provides an array of pre-calculated keys corresponding to the expanded values of 2^n for each index in the
        /// array. That is:
        ///     Keys[0] = 2^0 ( 1);
        ///     Keys[1] = 2^1 ( 2);
        ///     Keys[2] = 2^2 ( 4);
        ///     Keys[3] = 2^3 ( 8);
        ///     Keys[4] = 2^4 (16);
        ///     and so on.
        /// The array contains a maximum of 128 pre-calculated values, where the last element is:
        ///     Keys[127] = 2^127.
        /// </summary>
        public static readonly ModelYearKey[] Keys =
            (from i in Enumerable.Range(0, KeyCount)
             select NewKey(i >= 64 ? 1UL << i : 0,
                           i  < 64 ? 1UL << i : 0)).ToArray();

        /// <summary>
        /// Provides an array of pre-calculated sums of keys from 0 to a specific index. For example:
        ///     Keys[0] = 2^0 ( 1) --- KeySum[0] = 1               (bitwise:     1);
        ///     Keys[1] = 2^1 ( 2) --- KeySum[1] = 1+2        =  3 (bitwise:    11);
        ///     Keys[2] = 2^2 ( 4) --- KeySum[2] = 1+2+4      =  7 (bitwise:   111);
        ///     Keys[3] = 2^3 ( 8) --- KeySum[3] = 1+2+4+8    = 15 (bitwise:  1111);
        ///     Keys[4] = 2^4 (16) --- KeySum[4] = 1+2+4+8+16 = 31 (bitwise: 11111);
        ///     and so on.
        /// The array contains a maximum of 128 pre-calculated values, where the last element is:
        ///     KeySum[127] = 2^128 - 1 (bitwise: all 1's).
        /// </summary>
        public static readonly ModelYearKey[] KeySum =
            (from i in Enumerable.Range(0, KeyCount)
             select NewKey(i  < 64 ? ulong.MaxValue >> i : 0,
                           i >= 64 ? ulong.MaxValue >> i : ulong.MaxValue)).Reverse().ToArray();

        // ERROR strings
        private const string ModelYearTooSmall = "The value specified for model year is too small.";
        private const string ModelYearTooLarge = "The value specified for model year is too large.";

        //----- runtime variables -----
        private ulong _hi, _lo;

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets an empty <see cref="ModelYearKey"/> instance.</summary>
        public static ModelYearKey Empty => NewKey(0UL, 0UL);

        /// <summary>
        /// Gets a <see cref="ModelYearKey"/> instance where all model years, within the <see cref="MinYear"/> and
        /// <see cref="MaxYear"/> range, are set as used.
        /// </summary>
        public static ModelYearKey Full => KeySum.Last();

        /// <summary>Gets whether this <see cref="ModelYearKey"/> instance represents an empty key.</summary>
        public bool IsEmpty => this.Equals(Empty);

        /// <summary>
        /// Gets the first year defined by this <see cref="ModelYearKey"/> instance; or 0, if the
        /// <see cref="ModelYearKey"/> is empty.
        /// </summary>
        public int FirstYear
        {
            get {
                if (!this.IsEmpty) {
                    for (int year = MinYear; year <= MaxYear; year++) {
                        if (this.Contains(year)) {
                            return year;
                        }
                    }
                }
                return 0;
            }
        }
        /// <summary>
        /// Gets the last year defined by this <see cref="ModelYearKey"/> instance; or 0, if the
        /// <see cref="ModelYearKey"/> is empty.
        /// </summary>
        public int LastYear
        {
            get {
                if (!this.IsEmpty) {
                    for (int year = MaxYear; year >= MinYear; year--) {
                        if (this.Contains(year)) {
                            return year;
                        }
                    }
                }
                return 0;
            }
        }

        /// <summary>
        /// Gets or sets whether this <see cref="ModelYearKey"/> instance includes the specified year.
        /// </summary>
        /// <param name="year">The year for which to get or set the state.</param>
        /// <returns>
        /// true, if the specified year is a subset of this instance; false, otherwise.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The specified year is out-of-bounds of the <see cref="MinYear"/> and <see cref="MaxYear"/> range.
        ///     This exception is only thrown during a set operation.
        /// </exception>
        public bool this[int year]
        {
            get => this.Contains(year);
            set {
                var key = Keys[ToIndex(year)];
                if (value) {
                    this._hi |= key._hi;
                    this._lo |= key._lo;
                }
                else {
                    this._hi &= ~(key._hi);
                    this._lo &= ~(key._lo);
                }
            }
        }

        #endregion

        #region <<< Methods >>>

        #region < Overloaded Operators >

        /// <summary>Implements the operator ==.</summary>
        public static bool operator ==(ModelYearKey value1, ModelYearKey value2) => value1.Equals(value2);
        /// <summary>Implements the operator !=.</summary>
        public static bool operator !=(ModelYearKey value1, ModelYearKey value2) => !value1.Equals(value2);

        /// <summary>Implements the operator &gt;.</summary>
        public static bool operator >(ModelYearKey value1, ModelYearKey value2) => Compare(value1, value2) > 0;
        /// <summary>Implements the operator &lt;.</summary>
        public static bool operator <(ModelYearKey value1, ModelYearKey value2) => Compare(value1, value2) < 0;
        /// <summary>Implements the operator &gt;=.</summary>
        public static bool operator >=(ModelYearKey value1, ModelYearKey value2) => Compare(value1, value2) >= 0;
        /// <summary>Implements the operator &lt;=.</summary>
        public static bool operator <=(ModelYearKey value1, ModelYearKey value2) => Compare(value1, value2) <= 0;

        /// <summary>Implements the operator |.</summary>
        public static ModelYearKey operator |(ModelYearKey value1, ModelYearKey value2)
        {
            return NewKey(value1._hi | value2._hi,
                          value1._lo | value2._lo);
        }
        /// <summary>Implements the operator &amp;.</summary>
        public static ModelYearKey operator &(ModelYearKey value1, ModelYearKey value2)
        {
            return NewKey(value1._hi & value2._hi,
                          value1._lo & value2._lo);
        }
        /// <summary>Implements the operator ^.</summary>
        public static ModelYearKey operator ^(ModelYearKey value1, ModelYearKey value2)
        {
            return NewKey(value1._hi ^ value2._hi,
                          value1._lo ^ value2._lo);
        }
        /// <summary>Implements the operator ~.</summary>
        public static ModelYearKey operator ~(ModelYearKey value) => NewKey(~value._hi, ~value._lo);

        #endregion

        /// <summary>
        /// Determines whether the specified model year is within the bounds of the <see cref="MinYear"/> and
        /// <see cref="MaxYear"/> range.
        /// </summary>
        /// <param name="year">The model year to test.</param>
        /// <returns>
        /// true, if the specified year is within the bounds of the <see cref="MinYear"/> and <see cref="MaxYear"/>
        /// range; false, otherwise.
        /// </returns>
        public static bool IsValid(int year)
        {
            return (year >= MinYear && year <= MaxYear);
        }

        /// <summary>
        /// Builds a 128-bit key from the specified model years.
        /// </summary>
        /// <param name="years">One or more model years from which to compute a 128-bit key.</param>
        /// <returns>A 128-bit key.</returns>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The specified year is out-of-bounds of the <see cref="MinYear"/> and <see cref="MaxYear"/> range.
        /// </exception>
        public static ModelYearKey BuildKey(params int[] years)
        {
            if (years == null) {
                return Empty;
            }

            ulong hi = 0UL, lo = 0UL;
            foreach (int year in years) {
                int index = ToIndex(year);
                hi |= Keys[index]._hi;
                lo |= Keys[index]._lo;
            }
            return NewKey(hi, lo);
        }

        /// <summary>
        /// Sets whether this <see cref="ModelYearKey"/> instance includes the range of years given by the specified
        /// start and end years.
        /// </summary>
        /// <param name="startYear">
        ///     The inclusive starting year in a range of years to enable or disable. This value will be bounded
        ///     between <see cref="MinYear"/> and <see cref="MaxYear"/> before setting.
        /// </param>
        /// <param name="endYear">
        ///     The inclusive ending year in a range of years to enable or disable. This value will be bounded between
        ///     <see cref="MinYear"/> and <see cref="MaxYear"/> before setting.
        /// </param>
        /// <param name="state">
        ///     true, to enable the specified range of years within this <see cref="ModelYearKey"/> instance; false, to
        ///     disable the range of years in the <see cref="ModelYearKey"/>.
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">startYear must be less than or equal to endYear.</exception>
        public void SetRange(int startYear = MinYear, int endYear = MaxYear, bool state = false)
        {
            if (startYear > endYear) {
                throw new ArgumentOutOfRangeException(
                    "startYear|endYear", "startYear must be less than or equal to endYear.");
            }

            int sidx = ToIndex(startYear, true);
            int eidx = ToIndex(endYear, true);

            if (state) {
                this |= (~(sidx == 0 ? Empty : KeySum[sidx - 1]) & KeySum[eidx]);
            }
            else {
                this &= ((sidx == 0 ? Empty : KeySum[sidx - 1]) | ~KeySum[eidx]);
            }
        }

        /// <summary>
        /// Returns the string representation of this <see cref="ModelYearKey"/> instance.
        /// </summary>
        /// <returns>The string representation of the <see cref="ModelYearKey"/> instance.</returns>
        public override string ToString()
        {
            return KeyHelper.GetHexString(this._hi) + "-" +
                   KeyHelper.GetHexString(this._lo);
        }

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

        /// <summary>
        /// Determines whether the specified <see cref="object"/> is equal to the current <see cref="ModelYearKey"/>
        /// value.
        /// </summary>
        /// <param name="obj">
        ///     The <see cref="object"/> to compare with the current <see cref="ModelYearKey"/> value.
        /// </param>
        /// <returns>
        /// true, if the specified <see cref="object"/> is equal to the current <see cref="ModelYearKey"/> value;
        /// false, otherwise.
        /// </returns>
        public override bool Equals(object obj)
        {
            return (obj is ModelYearKey key) && this.Equals(key);
        }
        /// <summary>
        /// Determines whether the specified <see cref="ModelYearKey"/> value is equal to the current
        /// <see cref="ModelYearKey"/> value.
        /// </summary>
        /// <param name="value">
        ///     The <see cref="ModelYearKey"/> value to compare with the current <see cref="ModelYearKey"/> value.
        /// </param>
        /// <returns>
        /// true, if the specified <see cref="ModelYearKey"/> value is equal to the current <see cref="ModelYearKey"/>
        /// value; false, otherwise.
        /// </returns>
        public bool Equals(ModelYearKey value)
        {
            return (this._hi == value._hi
                 && this._lo == value._lo);
        }

        /// <summary>
        /// Determines whether this <see cref="ModelYearKey"/> instance contains the specified year.
        /// </summary>
        /// <param name="year">The year to test.</param>
        /// <returns>true, if the specified year is part of this instance; false, otherwise.</returns>
        public bool Contains(int year)
        {
            return (year >= MinYear
                 && year <= MaxYear
                 && this.Contains(Keys[year - MinYear]));
        }
        /// <summary>
        /// Determines whether this <see cref="ModelYearKey"/> instance contains the specified key.
        /// </summary>
        /// <param name="key">The key to test.</param>
        /// <returns>true, if key is a subset of this instance; false, otherwise.</returns>
        public bool Contains(ModelYearKey key)
        {
            return ((this._hi & key._hi) == key._hi
                 && (this._lo & key._lo) == key._lo);
        }

        /// <summary>
        /// Determines whether this <see cref="ModelYearKey"/> instance contains all years in the range between the
        /// specified <paramref name="startYear"/> and <paramref name="endYear"/> (inclusive).
        /// </summary>
        /// <param name="startYear">
        ///     The inclusive starting year in a range of years to test. This value will be bounded between
        ///     <see cref="MinYear"/> and <see cref="MaxYear"/> before testing.
        /// </param>
        /// <param name="endYear">
        ///     The inclusive ending year in a range of years to test. This value will be bounded between
        ///     <see cref="MinYear"/> and <see cref="MaxYear"/> before testing.
        /// </param>
        /// <returns>
        /// true, if all years between <paramref name="startYear"/> and <paramref name="endYear"/> (inclusive) are part
        /// of this instance; false, otherwise.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">startYear must be less than or equal to endYear.</exception>
        public bool ContainsAll(int startYear, int endYear)
        {
            if (startYear > endYear) {
                throw new ArgumentOutOfRangeException(
                    "startYear|endYear", "startYear must be less than or equal to endYear.");
            }

            int sIdx = ToIndex(startYear, true);
            int eIdx = ToIndex(endYear, true);
            ModelYearKey testKey = KeySum[eIdx] & (sIdx == 0 ? Full : ~KeySum[sIdx - 1]);

            return this.Contains(testKey);
        }

        /// <summary>
        /// Determines whether this <see cref="ModelYearKey"/> instance contains at least one year between the
        /// specified <paramref name="startYear"/> and <paramref name="endYear"/> (inclusive).
        /// </summary>
        /// <param name="startYear">
        ///     The inclusive starting year in a range of years to test. This value will be bounded between
        ///     <see cref="MinYear"/> and <see cref="MaxYear"/> before testing.
        /// </param>
        /// <param name="endYear">
        ///     The inclusive ending year in a range of years to test. This value will be bounded between
        ///     <see cref="MinYear"/> and <see cref="MaxYear"/> before testing.
        /// </param>
        /// <returns>
        /// true, if at least one year between <paramref name="startYear"/> and <paramref name="endYear"/> (inclusive)
        /// is part of this instance; false, otherwise.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">startYear must be less than or equal to endYear.</exception>
        public bool ContainsAny(int startYear, int endYear)
        {
            if (startYear > endYear) {
                throw new ArgumentOutOfRangeException(
                    "startYear|endYear", "startYear must be less than or equal to endYear.");
            }

            int sIdx = ToIndex(startYear, true);
            int eIdx = ToIndex(endYear, true);
            ModelYearKey testKey = KeySum[eIdx] & (sIdx == 0 ? Full : ~KeySum[sIdx - 1]);

            return (this & testKey) != Empty;
        }

        int IComparable.CompareTo(object value)
        {
            return (value == null) ? 1
                : (value is ModelYearKey key) ? Compare(this, key)
                : throw new ArgumentException("Value must be ModelYearKey");
        }
        int IComparable<ModelYearKey>.CompareTo(ModelYearKey value)
        {
            return Compare(this, value);
        }

        static int Compare(ModelYearKey value1, ModelYearKey value2)
        {
            return (value1._hi != value2._hi)
                ? value1._hi.CompareTo(value2._hi)
                : value1._lo.CompareTo(value2._lo);
        }

        /// <summary>
        /// Converts year to index. If safeBounds is true, year will be clipped at MinYear/MaxYear if out-of-bounds;
        /// if safeBounds is false, an exception will be thrown if year is out-of-bounds.
        /// </summary>
        static int ToIndex(int year, bool safeBounds = false)
        {
            if (safeBounds) {
                year = Math.Max(MinYear, Math.Min(year, MaxYear));
            }
            else {
                CheckBounds(year);
            }
            return year - MinYear;
        }

        static void CheckBounds(int year)
        {
            if (year < MinYear) {
                throw new ArgumentOutOfRangeException("year", year, ModelYearTooSmall);
            }
            if (year > MaxYear) {
                throw new ArgumentOutOfRangeException("year", year, ModelYearTooLarge);
            }
        }

        static ModelYearKey NewKey(ulong hi, ulong lo)
        {
            return new ModelYearKey() { _hi = hi, _lo = lo };
        }

        #endregion
    }
}
