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

using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.ModelSettings;

using TI = Volpe.Cafe.TechnologyIndexes;

namespace Volpe.Cafe.TechAnalysis
{
    /// <summary>
    /// Provides helper functions for evaluating applicability of candidate technologies on <see cref="IOEMProduct"/>s
    /// produced by a manufacturer.
    /// </summary>
    public static class TechEvaluator
    {
        /// <summary>
        /// Determines whether a technology with the specified index is applicable on a given vehicle during a standard
        /// setting year.
        /// </summary>
        /// <param name="techIndex">The index of a technology to check for applicability on a vehicle.</param>
        /// <param name="vehInfo">The vehicle for which to determine technology applicability.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        /// <returns>
        /// <c>true</c>, if a technology with the specified index is applicable on a given vehicle during a standard
        /// setting year; <c>false</c>, otherwise.
        /// </returns>
        public static bool IsTechApplicableInStandardSettingYear(
            int techIndex, IVehicleInfo vehInfo, ModelingSettings settings)
        {
            if (vehInfo.RegClass.IsHDPUV()) {
                return true;
            }
            if (vehInfo.HevType.IsBevOrFcv()) {
                return false;
            }
            if (vehInfo.ZEVCandidate.HasValue) {
                return true;
            }

            bool isBevFcv    = TI.IsConversionToBevOrFcv(techIndex);

            return !isBevFcv;
        }

        /// <summary>
        /// Determines whether a technology with the specified index is valid for application on a given
        /// <see cref="IVehicle"/>.
        /// </summary>
        /// <param name="techIndex">The index of a technology to check for applicability on a vehicle.</param>
        /// <param name="veh">The vehicle for which to determine technology applicability.</param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="analysisYear">The model year being analyzed.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        /// <param name="windowOfApplication">
        ///     When this method returns, contains the window of application (as min/max model years) during which a
        ///     specific technology was selected for application on a vehicle. If a technology is not valid for
        ///     application on a given vehicle, this value will instead be <see cref="WindowOfApplication.Empty"/>.
        /// </param>
        /// <param name="affectedVehicle">
        ///     When this method returns, contains a vehicle affected by application of a specific technology, along
        ///     with the initial year when the technology may be applied on that vehicle. If a technology is not valid
        ///     for application on a given vehicle, this value will instead be <see cref="AffectedVehicle.Empty"/>.
        /// </param>
        /// <returns>
        /// <c>true</c>, if a technology with the specified index is valid for application on a given
        /// <see cref="IVehicle"/>; <c>false</c>, otherwise.
        /// </returns>
        public static bool IsTechValid(
            int techIndex, IVehicle veh, Scenario scen, int analysisYear, ModelingSettings settings,
            out WindowOfApplication windowOfApplication, out AffectedVehicle affectedVehicle)
        {
            return IsTechValid(techIndex, veh, scen, analysisYear, settings, false,
                               out windowOfApplication, out affectedVehicle);
        }

        /// <summary>
        /// Determines whether a technology with the specified index is valid for application on a given
        /// <see cref="IVehicle"/>.
        /// </summary>
        /// <param name="techIndex">The index of a technology to check for applicability on a vehicle.</param>
        /// <param name="veh">The vehicle for which to determine technology applicability.</param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="analysisYear">The model year being analyzed.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        /// <param name="forced">
        ///     Indicates whether non-vehicle-level technologies (e.g., engine-level) should be forced for evaluation
        ///     on a vehicle.
        /// </param>
        /// <param name="windowOfApplication">
        ///     When this method returns, contains the window of application (as min/max model years) during which a
        ///     specific technology was selected for application on a vehicle. If a technology is not valid for
        ///     application on a given vehicle, this value will instead be <see cref="WindowOfApplication.Empty"/>.
        /// </param>
        /// <param name="affectedVehicle">
        ///     When this method returns, contains a vehicle affected by application of a specific technology, along
        ///     with the initial year when the technology may be applied on that vehicle. If a technology is not valid
        ///     for application on a given vehicle, this value will instead be <see cref="AffectedVehicle.Empty"/>.
        /// </param>
        /// <returns>
        /// <c>true</c>, if a technology with the specified index is valid for application on a given
        /// <see cref="IVehicle"/>; <c>false</c>, otherwise.
        /// </returns>
        public static bool IsTechValid(
            int techIndex, IVehicle veh, Scenario scen, int analysisYear, ModelingSettings settings, bool forced,
            out WindowOfApplication windowOfApplication, out AffectedVehicle affectedVehicle)
        {
            windowOfApplication = WindowOfApplication.Empty;
            affectedVehicle = AffectedVehicle.Empty;

            // even though all techs can be installed on a vehicle,
            // we want to separate component and vehicle-level evaluation
            if (!forced && !TI.IsVehicleLevel(techIndex)) {
                return false;
            }

            var techUse = veh.TechUtilization[techIndex];
            var mfrRevision = veh.Manufacturer.GetRevision(analysisYear);
            var runtimeSettings = settings.RuntimeSettings;
            int startYear = runtimeSettings.AnalysisStartYear;
            int endYear = runtimeSettings.AnalysisEndYear;

            // initial check to ensure the tech can be applied on the vehicle
            if (!techUse.IsAvailable(startYear, endYear: analysisYear)
                    || techUse.IsUsed(analysisYear)
                    || techUse.IsIgnored
                    || mfrRevision.TechExhausted[techIndex]) {
                return false;
            }

            // do not allow application of ZEV techs (PHEV, BEV, or FCV) when evaluating a
            // standard setting year, even if the tech is available in a preceding non-standard
            // setting year (as in a multiyear solution)
            // ... unless vehicle is a zev candidate or regulated as HDPUV
            var vehRevision = veh.GetRevision(analysisYear);
            if (scen[vehRevision.RegClass, analysisYear].StandardSettingYear
                    && !IsTechApplicableInStandardSettingYear(techIndex, vehRevision, settings)) {
                return false;
            }

            windowOfApplication = GetWindowOfApplication(techIndex, veh, analysisYear, startYear, endYear);
            if (!windowOfApplication.IsValid) {
                return false;
            }
            if (windowOfApplication.IsMultiYear) {
                // for multiyear solution, we now need to ensure the tech is
                // not exhausted in the earlier year
                mfrRevision = veh.Manufacturer.GetRevision(windowOfApplication.MinYear);
                if (mfrRevision.TechExhausted[techIndex]) {
                    return false;
                }
            }

            affectedVehicle = new AffectedVehicle(veh, windowOfApplication.MinYear);

            return true;
        }

        /// <summary>
        /// Determines whether a technology with the specified index is valid for application on a given
        /// <see cref="IOEMComponent"/>.
        /// </summary>
        /// <param name="techIndex">The index of a technology to check for applicability on a component.</param>
        /// <param name="component">The component for which to determine technology applicability.</param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="analysisYear">The model year being analyzed.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        /// <param name="windowOfApplication">
        ///     When this method returns, contains the window of application (as min/max model years) during which a
        ///     specific technology was selected for application on a component. If a technology is not valid for
        ///     application on a given component, this value will instead be <see cref="WindowOfApplication.Empty"/>.
        /// </param>
        /// <param name="affectedVehicles">
        ///     When this method returns, contains one or more vehicles affected by application of a specific
        ///     technology, along with the initial years when the technology may be applied on those vehicles. If a
        ///     technology is not valid for application on a given component, this value will instead be <c>null</c>.
        /// </param>
        /// <returns>
        /// <c>true</c>, if a technology with the specified index is valid for application on a given
        /// <see cref="IOEMComponent"/>; <c>false</c>, otherwise.
        /// </returns>
        public static bool IsTechValid(
            int techIndex, IOEMComponent component, Scenario scen, int analysisYear, ModelingSettings settings,
            out WindowOfApplication windowOfApplication, out IEnumerable<AffectedVehicle> affectedVehicles)
        {
            windowOfApplication = WindowOfApplication.Empty;
            affectedVehicles = null;

            var techUse = component.TechUtilization[techIndex];
            if (techUse == null) {
                // components only define tech-utilization for techs that
                // directly apply to them (e.g., eng-level techs for engs)
                return false;
            }

            var mfrRevision = component.Manufacturer.GetRevision(analysisYear);
            var runtimeSettings = settings.RuntimeSettings;
            int startYear = runtimeSettings.AnalysisStartYear;
            int endYear = runtimeSettings.AnalysisEndYear;

            // initial check to ensure the tech can be applied on the component
            if (!techUse.IsAvailable(startYear, endYear: analysisYear)
                    || techUse.IsUsed(analysisYear)
                    || techUse.IsIgnored
                    || mfrRevision.TechExhausted[techIndex]) {
                return false;
            }

            windowOfApplication = GetWindowOfApplication(techIndex, component, analysisYear, startYear, endYear);
            if (!windowOfApplication.IsValid) {
                return false;
            }
            if (windowOfApplication.IsMultiYear) {
                // for multiyear solution, we now need to ensure the tech is
                // not exhausted in the earlier year
                mfrRevision = component.Manufacturer.GetRevision(windowOfApplication.MinYear);
                if (mfrRevision.TechExhausted[techIndex]) {
                    return false;
                }
            }

            var (minYear, maxYear) = windowOfApplication;
            affectedVehicles = component.Vehicles
                .Where(veh => IsAffectedVehicle(veh))
                .Select(veh => CreateAffectedVehicle(veh));

            bool IsAffectedVehicle(IVehicle veh)
            {
                if (!veh.TechUtilization[techIndex].IsAvailable(minYear, maxYear)) {
                    return false;
                }

                var vehRevision = veh.GetRevision(analysisYear);
                return !scen[vehRevision.RegClass, analysisYear].StandardSettingYear
                    || IsTechApplicableInStandardSettingYear(techIndex, vehRevision, settings);
            }

            AffectedVehicle CreateAffectedVehicle(IVehicle veh)
            {
                return new AffectedVehicle(
                    veh, veh.TechUtilization[techIndex].GetFirstYearAvailable(minYear, maxYear));
            }

            //------------------------------------------------------------------------------------
            // *** HACK-HACK-HACK ***
            // We have a special case for HDPUV vehicles. For HDPUV, there should be a progression
            // from SGDI to DEAC (for LD, there isn't). Meaning, DEAC cannot exist by itself on
            // HDPUV vehicles -- SGDI and SGDI+DEAC can. This causes issues with rule-based logic
            // defined in tech-indexes class, because LD vehicles do allow DEAC by itself. As a
            // hack, when evaluating DEAC on HDPUV vehicles, we consider DEAC as "invalid" for
            // application if SGDI is not already used on that vehicle.
            //------------------------------------------------------------------------------------
            if (techIndex == TI.DEAC) {
                affectedVehicles = affectedVehicles.Where(
                    item => !item.Vehicle.GetRevision(analysisYear).RegClass.IsHDPUV()
                          || item.Vehicle.TechUtilization[TI.SGDI].IsUsed(minYear));
            }

            return true;
        }

        /// <summary>
        /// Gets the window of application for a technology with the specified <paramref name="techIndex"/>.
        /// <br/><br/>
        /// <b>Precondition</b>:
        ///     The technology being evaluated is assumed to be available for application on the specific
        ///     <paramref name="product"/>. That is, the availability check should be performed by the caller prior to
        ///     computing the window of application.
        /// </summary>
        private static WindowOfApplication GetWindowOfApplication(
            int techIndex, IOEMProduct product, int analysisYear,
            int startYear = ModelYearKey.MinYear, int endYear = ModelYearKey.MaxYear)
        {
            var techUse = product.TechUtilization[techIndex];
            if (techUse.IsAvailable(analysisYear)) {
                return (analysisYear, analysisYear);
            }

            // NOTE: For now, we want to trim maxYear at analysisYear. Later, we'll add a
            // runtime switch to use "expanded" window of application. For this, we'll need to
            // modify CF logic a bit further in order to apply valuation to credits during
            // future model years.
            int minYear = techUse.GetLastYearAvailable(startYear, analysisYear - 1);
            int maxYear = analysisYear;
            //int maxYear = (analysisYear == endYear)
            //            ? endYear
            //            : techUse.GetFirstYearAvailable(analysisYear + 1, endYear) - 1;

            return new WindowOfApplication(minYear, maxYear);
        }

    }
}
