#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

using System;
using System.Collections.Generic;
using System.Linq;

namespace Volpe.Cafe.Generic
{
    /// <summary>
    /// Represents an object that stores modeling data as a continuous stream of annual values.
    /// </summary>
    /// <typeparam name="T">The type of elements in the stream of annual values.</typeparam>
    [Serializable]
    public class AnnualValue<T> : IAnnualValue<T>
    {
        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="AnnualValue{T}"/> class using the specified parameters.
        /// </summary>
        /// <param name="minYear">The lowest year for which annual values are defined.</param>
        /// <param name="maxYear">The highest year for which annual values are defined.</param>
        /// <param name="values">
        ///     An list of annual values corresponding to the specified minYear and maxYear. If values is null, it will
        ///     be initialized based on the range of years defined by minYear and maxYear.
        /// </param>
        /// <param name="outOfBoundsMode">
        ///     Defines the way this instance will handle requested years that are out-of-bounds of the
        ///     <see cref="MinYear"/> and <see cref="MaxYear"/> range. See <see cref="OutOfBoundsMode"/> for more
        ///     information.
        /// </param>
        /// <exception cref="ArgumentException">
        ///     The specified minYear and maxYear do not correspond to the length of the values array.
        ///     -or-
        ///     maxYear cannot be less than minYear.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException"> maxYear and minYear cannot be less than zero.</exception>
        public AnnualValue(int minYear, int maxYear, IList<T> values = null, int outOfBoundsMode = 0)
        {
            if (maxYear < minYear) {
                throw new ArgumentException("maxYear cannot be less than minYear.", "minYear|maxYear");
            }
            if (maxYear < 0 || minYear < 0) {
                throw new ArgumentOutOfRangeException(
                    "minYear|maxYear", "maxYear and minYear cannot be less than zero.");
            }

            int yearCount = maxYear - minYear + 1;
            if (values != null && yearCount != values.Count) {
                throw new ArgumentException(
                    "The specified minYear and maxYear do not correspond to the length of the values array.",
                    "minYear|maxYear|values");
            }

            this.MinYear = minYear;
            this.MaxYear = maxYear;
            this.OutOfBoundsMode = outOfBoundsMode;
            this.Values = (values == null) ? new T[yearCount] : values.ToArray();
        }

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets or sets a value associated with the specified year.</summary>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The specified year is out-of-bounds of the <see cref="MinYear"/> and <see cref="MaxYear"/> range.
        ///     During a <b>set</b> operation, this exception is always thrown if the specified year is out-of-bounds.
        ///     During a <b>get</b> operation, this exception is only thrown when the <see cref="OutOfBoundsMode"/> is
        ///     defined to throw exceptions for out-of-bounds values.
        /// </exception>
        public T this[int year]
        {
            get {
                if (this.HasValue(year)) {
                    return this.Values[year - this.MinYear];
                }

                switch (this.OutOfBoundsMode) {
                    case 0: return default;
                    case 1: return (year < this.MinYear) ? this.Values.FirstOrDefault() : this.Values.LastOrDefault();
                    case 2: return (year < this.MinYear) ? default : this.Values.LastOrDefault();

                    default:
                        throw this.YearOutOfBounds();
                }
            }
            set {
                this.Values[year - this.MinYear] = this.HasValue(year) ? value : throw this.YearOutOfBounds();
            }
        }

        public int MinYear { get; private set; }
        public int MaxYear { get; private set; }

        public T MinValue => this.Values.First();
        public T MaxValue => this.Values.Last();

        public int OutOfBoundsMode { get; private set; }

        private T[] Values { get; set; }

        #endregion

        #region <<< Methods >>>

        /// <summary>
        /// Returns the string representation of this <see cref="AnnualValue{T}"/> instance.
        /// </summary>
        /// <returns>The string representation of the <see cref="AnnualValue{T}"/> instance.</returns>
        public override string ToString()
        {
            return $"{{MinYear={this.MinYear}, MaxYear={this.MaxYear}, OutOfBoundsMode={this.OutOfBoundsMode}}}";
        }

        public bool HasValue(int year)
        {
            return (this.MinYear <= year && year <= this.MaxYear);
        }

        /// <summary>
        /// Converts all elements in the current <see cref="AnnualValue{T}"/> instance to another type, and returns a
        /// new <see cref="AnnualValue{T}"/> object containing the converted elements.
        /// </summary>
        /// <param name="converter">
        ///     A <see cref="Converter{T, TOutput}"/> delegate that converts each element from one type to another
        ///     type.
        /// </param>
        /// <param name="newOutOfBoundsMode">
        ///     An <see cref="OutOfBoundsMode"/> to use in the new <see cref="AnnualValue{T}"/> object, or null, to use
        ///     the <see cref="OutOfBoundsMode"/> from the current instance.
        /// </param>
        /// <typeparam name="TOutput">
        ///     The target type of the converted elements in the new stream of annual values.
        /// </typeparam>
        /// <returns>
        /// An <see cref="AnnualValue{TOutput}"/> object of the target type containing the converted elements from the
        /// current instance.
        /// </returns>
        /// <exception cref="ArgumentNullException">converter is null.</exception>
        public AnnualValue<TOutput> ConvertAll<TOutput>(
            Converter<T, TOutput> converter, int? newOutOfBoundsMode = null)
        {
            TOutput[] convertedValues = Array.ConvertAll(this.Values, converter);
            int outOfBoundsMode = newOutOfBoundsMode ?? this.OutOfBoundsMode;

            return new AnnualValue<TOutput>(this.MinYear, this.MaxYear, convertedValues, outOfBoundsMode);
        }

        /// <summary>
        /// Returns a <see cref="ReadOnlyAnnualValue{T}"/> object that acts as a read-only wrapper around the current
        /// <see cref="AnnualValue{T}"/> instance.
        /// </summary>
        public ReadOnlyAnnualValue<T> AsReadOnly()
        {
            return new ReadOnlyAnnualValue<T>(this);
        }

        private Exception YearOutOfBounds()
        {
            throw new ArgumentOutOfRangeException("year",
                "The specified year is out-of-bounds of the MinYear and MaxYear range.");
        }

        //----- explicit interface implementation -----
        IAnnualValue<TOutput> IAnnualValue<T>.ConvertAll<TOutput>(Converter<T, TOutput> converter, int? newOobMode)
        {
            return this.ConvertAll(converter, newOobMode);
        }

        #endregion
    }
}
