using System;
using Volpe.Cafe.MarketData.Design;

namespace Volpe.Cafe
{
    /// <summary>
    /// Provides information about utilization of a single technology on an <see cref="IOEMProduct"/>. This includes
    /// whether a technology is presently in use, when it was applied, if and when it was inherited or superseded, and
    /// whether it has been disabled from application during all model years. Additionally, if a technology is not
    /// disabled, utilization information also indicates the years of availability for a specific
    /// <see cref="IOEMProduct"/>.
    /// </summary>
    [Serializable]
    public class TechnologyUtilization
    {
        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="TechnologyUtilization"/> class, using the specified technology
        /// utilization information.
        /// </summary>
        /// <param name="isUsed">
        ///     Specifies whether the technology is initially used on an <see cref="IOEMProduct"/>.
        /// </param>
        /// <param name="isDisabled">
        ///     Specifies whether a technology is disabled from application on an <see cref="IOEMProduct"/> in all
        ///     model years.
        /// </param>
        /// <param name="yearsAvailable">
        ///     An array of model years specifying when a technology is available for application on an
        ///     <see cref="IOEMProduct"/>, taking into account the redesign and refresh years, the technology's
        ///     applicability, as well as any other modeling and engineering restrictions pertinent to that
        ///     <see cref="IOEMProduct"/>.
        /// </param>
        public TechnologyUtilization(bool isUsed, bool isDisabled, int[] yearsAvailable)
            : this(isUsed, isDisabled, (isUsed || isDisabled) ? ModelYearKey.Empty
                                                              : ModelYearKey.BuildKey(yearsAvailable))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TechnologyUtilization"/> class, using the specified technology
        /// utilization information.
        /// </summary>
        /// <param name="isUsed">
        ///     Specifies whether the technology is initially used on an <see cref="IOEMProduct"/>.
        /// </param>
        /// <param name="isDisabled">
        ///     Specifies whether a technology is disabled from application on an <see cref="IOEMProduct"/> in all
        ///     model years.
        /// </param>
        /// <param name="yearsAvailable">
        ///     A key of model years specifying when a technology is available for application on an
        ///     <see cref="IOEMProduct"/>, taking into account the redesign and refresh years, the technology's
        ///     applicability, as well as any other modeling and engineering restrictions pertinent to that
        ///     <see cref="IOEMProduct"/>.
        /// </param>
        /// <remarks>
        /// <b>NOTES</b>:<br/>
        /// A technology that is initially in use will be set as disabled as well.<br/>
        /// Additionally, if a technology is initially in use or is disabled on an <see cref="IOEMProduct"/>, the
        /// yearsAvailable parameter will be ignored.
        /// </remarks>
        public TechnologyUtilization(bool isUsed, bool isDisabled = false, ModelYearKey? yearsAvailable = null)
        {
            this._yearsAvailable = (isUsed || isDisabled || !yearsAvailable.HasValue)
                                 ? ModelYearKey.Empty
                                 : yearsAvailable.Value;

            this.YearApplied = (isUsed) ? SetInBaseline : 0;
            this.YearInherited = 0;
            this.YearSuperseded = 0;
            this.IsDisabled = (isUsed || isDisabled);
        }

        #endregion

        #region <<< Variables >>>

        /// <summary>
        /// Specifies that the technology was set as used or superseded in the baseline fleet.
        /// </summary>
        public const int SetInBaseline = 1;

        /// <summary>
        /// Specifies the model years specifying when a technology is available for application on an
        /// <see cref="IOEMProduct"/>, taking into account the redesign and refresh years, the technology's
        /// applicability, as well as any other modeling and engineering restrictions pertinent to that
        /// <see cref="IOEMProduct"/>.
        /// </summary>
        private ModelYearKey _yearsAvailable;

        #endregion

        #region <<< Properties >>>

        /// <summary>
        /// Gets whether a technology is disabled from application in all model years.
        /// </summary>
        /// <remarks>
        /// A disabled technology is not available for application, typically because it is not compatible with a
        /// vehicle's or component's configuration.
        /// </remarks>
        public bool IsDisabled { get; private set; }

        /// <summary>
        /// Gets or sets whether a technology has been temporarily turned off, and will no longer be used in the model
        /// year that is being analyzed.
        /// </summary>
        /// <remarks>
        /// <b>NOTE</b>: The compliance model may temporarily turn off a technology if application of that technology:
        /// (1) does not lead to any vehicle fuel savings; (2) causes a reduction in credit-valuation; (3) adversely
        /// affects the compliance state of a manufacturer (i.e., decreases compliance); or (4) is not cost-effective
        /// but the manufacturer is already in compliance in the regulatory classes being evaluated.
        /// </remarks>
        public bool IsIgnored { get; set; }

        /// <summary>
        /// Gets the model year when a technology was applied on an <see cref="IOEMProduct"/>. If this value is 1, the
        /// technology was applied in the baseline; if this value is 0, the technology has not been applied.
        /// </summary>
        public int YearApplied { get; private set; }
        /// <summary>
        /// Gets the model year when a technology was inherited on an <see cref="IOEMProduct"/>. Inheritance typically
        /// propagates from a component to a vehicle, or from a component family to a component. If this value is 0,
        /// the technology has not been inherited.
        /// </summary>
        public int YearInherited { get; private set; }
        /// <summary>
        /// Gets the model year when a technology was superseded on an <see cref="IOEMProduct"/> by another technology.
        /// If this value is 1, the technology was superseded in the baseline (this typically should not happen); if
        /// this value is 0, the technology has not been superseded.
        /// </summary>
        public int YearSuperseded { get; private set; }

        #endregion

        #region <<< Methods >>>

        /// <summary>
        /// Returns the string representation of this <see cref="TechnologyUtilization"/> instance.
        /// </summary>
        /// <returns>The string representation of the <see cref="TechnologyUtilization"/> instance.</returns>
        public override string ToString()
        {
            return
                "{IsDisabled="          + this.IsDisabled +
                ", FirstYearAvailable=" + this._yearsAvailable.FirstYear +
                ", LastYearAvailable="  + this._yearsAvailable.LastYear +
                ", YearApplied="        + this.YearApplied +
                ", YearInherited="      + this.YearInherited +
                ", YearSuperseded="     + this.YearSuperseded + "}";
        }

        //----- get availability/utilization -----        
        /// <summary>
        /// Determines whether a technology is available for application in any model year.
        /// </summary>
        /// <returns>
        /// true, if a technology is available for application in any model year; false, otherwise.
        /// </returns>
        public bool IsAvailable()
        {
            return this.IsAvailable(-1);
        }
        /// <summary>
        /// Determines whether a technology is available for application in the specified model year.
        /// </summary>
        /// <param name="year">
        ///     The model year for which to test a technology's availability. Specify -1 to test if a technology is
        ///     available for application in any model year.
        /// </param>
        /// <returns>
        /// true, if a technology is available for application in the specified model year; false, otherwise.
        /// </returns>
        public bool IsAvailable(int year)
        {
            return !this.IsDisabled
                && ((year == -1) ? !this._yearsAvailable.IsEmpty : this._yearsAvailable.Contains(year));
        }
        /// <summary>
        /// Determines whether a technology is available for application within the specified range of model years.
        /// </summary>
        /// <param name="startYear">The inclusive starting year in a range of model years to test.</param>
        /// <param name="endYear">The inclusive ending year in a range of model years to test.</param>
        /// <returns>
        /// true, if a technology is available for application within the specified range of model years; 
        /// false, otherwise.
        /// </returns>
        public bool IsAvailable(int startYear, int endYear)
        {
            return !this.IsDisabled
                && this._yearsAvailable.ContainsAny(startYear, endYear);
        }

        /// <summary>
        /// Gets the first model year when a technology is available for application, within the specified range of
        /// model years.
        /// </summary>
        /// <param name="startYear">The inclusive starting year in a range of model years to search.</param>
        /// <param name="endYear">The inclusive ending year in a range of model years to search.</param>
        /// <returns>
        /// The first model year when a technology is available for application; or 0, if the technology is unavailable
        /// within the specified range.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">startYear must be less than or equal to endYear.</exception>
        public int GetFirstYearAvailable(int startYear, int endYear)
        {
            if (startYear > endYear) {
                throw new ArgumentOutOfRangeException(
                    "startYear|endYear", "startYear must be less than or equal to endYear.");
            }

            startYear = Math.Max(startYear, ModelYearKey.MinYear);
            endYear = Math.Min(endYear, ModelYearKey.MaxYear);

            for (int year = startYear; year <= endYear; year++) {
                if (this.IsAvailable(year)) {
                    return year;
                }
            }
            return 0;
        }

        /// <summary>
        /// Gets the last model year when a technology is available for application, within the specified range of
        /// model years.
        /// </summary>
        /// <param name="startYear">The inclusive starting year in a range of model years to search.</param>
        /// <param name="endYear">The inclusive ending year in a range of model years to search.</param>
        /// <returns>
        /// The last model year when a technology is available for application; or 0, if the technology is unavailable
        /// within the specified range.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">startYear must be less than or equal to endYear.</exception>
        public int GetLastYearAvailable(int startYear, int endYear)
        {
            if (startYear > endYear) {
                throw new ArgumentOutOfRangeException(
                    "startYear|endYear", "startYear must be less than or equal to endYear.");
            }

            startYear = Math.Max(startYear, ModelYearKey.MinYear);
            endYear = Math.Min(endYear, ModelYearKey.MaxYear);

            for (int year = endYear; year >= startYear; year--) {
                if (this.IsAvailable(year)) {
                    return year;
                }
            }
            return 0;
        }

        /// <summary>
        /// Determines whether a technology is set as used in the baseline fleet.
        /// </summary>
        /// <returns>true, if a technology is set as used in the baseline fleet; false, otherwise.</returns>
        public bool IsUsedInBaseline()
        {
            return this.YearApplied == SetInBaseline;
        }
        /// <summary>
        /// Determines whether a technology is set as used in the specified model year.
        /// </summary>
        /// <param name="year">The model year for which to test a technology's utilization.</param>
        /// <returns>true, if a technology is set as used in the specified model year; false, otherwise.</returns>
        public bool IsUsed(int year)
        {
            return this.YearApplied != 0 && this.YearApplied <= year;
        }
        /// <summary>
        /// Determines whether a technology is set as applied in the specified model year.
        /// </summary>
        /// <param name="year">The model year for which to test a technology's utilization.</param>
        /// <returns>true, if a technology is set as applied in the specified model year; false, otherwise.</returns>
        /// <remarks>
        /// An applied state indicates that the technology was set as used during modeling, but was not in use in the
        /// baseline fleet.
        /// </remarks>
        public bool IsApplied(int year)
        {
            return this.YearApplied > SetInBaseline && this.YearApplied <= year;
        }

        /// <summary>
        /// Determines whether a technology is set as inherited in the specified model year.
        /// </summary>
        /// <param name="year">The model year for which to test a technology's utilization.</param>
        /// <returns>true, if a technology is set as inherited in the specified model year; false, otherwise.</returns>
        public bool IsInherited(int year)
        {
            return this.YearInherited != 0 && this.YearInherited <= year;
        }
        /// <summary>
        /// Determines whether a technology is set as superseded in the specified model year.
        /// </summary>
        /// <param name="year">The model year for which to test a technology's utilization.</param>
        /// <returns>
        ///     true, if a technology is set as superseded in the specified model year; false, otherwise.
        /// </returns>
        public bool IsSuperseded(int year)
        {
            return this.YearSuperseded != 0 && this.YearSuperseded <= year;
        }

        /// <summary>
        /// Gets a technology's applicability and utilization state in the specified model year.
        /// </summary>
        /// <param name="year">
        ///     The model year for which to obtain a technology's applicability and utilization state.
        /// </param>
        /// <returns>A technology's applicability and utilization state in the specified model year.</returns>
        public TechnologyState GetState(int year)
        {
            var state = TechnologyState.Unavailable;
            if (this.IsAvailable(year)) {
                state = TechnologyState.Available;
            }
            else {
                if (this.IsUsed      (year)) { state |= TechnologyState.Used; }
                if (this.IsInherited (year)) { state |= TechnologyState.Inherited; }
                if (this.IsSuperseded(year)) { state |= TechnologyState.Superseded; }
            }
            return state;
        }

        //----- set availability/utilization -----
        /// <summary>
        /// Disables a technology from application in all model years.
        /// </summary>
        /// <remarks>
        /// A technology should only be disabled if it is not compatible with the <see cref="IOEMProduct"/>
        /// configuration. For example, engine and transmission upgrades are not compatible with electric vehicles.
        /// Hence, provided the technologies input file is configured correctly, engine and transmission technologies
        /// should be disabled on an electric vehicle.
        /// </remarks>
        public void SetDisabled()
        {
            this._yearsAvailable = ModelYearKey.Empty;
            this.IsDisabled = true;
        }

        /// <summary>
        /// Sets a technology as unavailable for application in all model years.
        /// </summary>
        public void SetUnavailable()
        {
            this._yearsAvailable = ModelYearKey.Empty;
        }
        /// <summary>
        /// Sets a technology as unavailable for application starting with the specified model year.
        /// </summary>
        /// <param name="year">
        ///     The model year starting with which a technology will be set as unavailable for application.
        /// </param>
        public void SetUnavailable(int year)
        {
            this._yearsAvailable.SetRange(startYear: year, state: false);
        }
        /// <summary>
        /// Sets a technology as unavailable for application in the specified model years.
        /// </summary>
        /// <param name="yearsUnavailable">
        ///     A key of model years specifying when a technology is not available for application on an 
        ///     <see cref="IOEMProduct"/>.
        /// </param>
        public void SetUnavailable(ModelYearKey yearsUnavailable)
        {
            this._yearsAvailable &= ~yearsUnavailable;
        }

        /// <summary>
        /// Sets a technology as used starting at the specified model year.
        /// </summary>
        /// <param name="year">The model year when a technology was initially applied.</param>
        /// <exception cref="InvalidOperationException">
        ///     Disabled technologies may not be set as used.
        ///     -or-
        ///     Previously used technologies may not be re-used.
        /// </exception>
        public void SetUsed(int year)
        {
            if (this.IsDisabled) {
                throw new InvalidOperationException("Disabled technologies may not be set as used.");
            }
            if (this.YearApplied != 0) {
                throw new InvalidOperationException("Previously used technologies may not be re-used.");
            }

            this.YearApplied = year;

            // once a technology is set as used/applied, it may no longer be
            // considered for application, starting with that year
            this.SetUnavailable(year);
        }
        /// <summary>
        /// Sets a technology as inherited starting at the specified model year.
        /// </summary>
        /// <param name="year">The model year when a technology was initially inherited.</param>
        /// <exception cref="InvalidOperationException">
        ///     Disabled technologies may not be set as used.
        ///     -or-
        ///     Previously used technologies may not be re-used.
        /// </exception>
        public void SetInherited(int year)
        {
            if (!this.IsUsed(year)) {
                this.SetUsed(year);
            }
            this.YearInherited = year;
        }
        /// <summary>
        /// Sets a technology as superseded starting at the specified model year.
        /// </summary>
        /// <param name="year">The model year when a technology was initially superseded.</param>
        /// <exception cref="InvalidOperationException">Unused technologies may not be set as superseded.</exception>
        /// <exception cref="ArgumentException">Year superseded may not be less than year applied.</exception>
        public void SetSuperseded(int year)
        {
            if (this.YearApplied == 0) {
                throw new InvalidOperationException("Unused technologies may not be set as superseded.");
            }
            if (year < this.YearApplied) {
                throw new ArgumentException("Year superseded may not be less than year applied.", "year");
            }

            this.YearSuperseded = year;
        }

        #endregion
    }
}
