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

using TechKey = Volpe.Cafe.TechKeys.TechKey128;
using TI = Volpe.Cafe.TechnologyIndexes;

namespace Volpe.Cafe.TechAnalysis
{
    /// <summary>
    /// Provides helper functions for initializing technology availability and utilization for a manufacturer and
    /// various <see cref="IOEMProduct"/>s produced by a manufacturer.
    /// </summary>
    public static class TechUseInitializer
    {
        /// <summary>
        /// Initializes the technology availability and utilization for all vehicles and components of a specified
        /// <see cref="IManufacturer"/>.
        /// </summary>
        /// <param name="mfr">
        ///     The <see cref="IManufacturer"/> whose technology availability and utilization to initialize.
        /// </param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        public static void InitTechUtilization(IManufacturer mfr, Scenario scen, ModelingSettings settings)
        {
            foreach (var item in mfr.Vehicles) { InitTechUtilization(item, scen, settings); }
            foreach (var item in mfr.Platforms) { InitTechUtilization(item, scen, settings); }
            foreach (var item in mfr.Engines) { InitTechUtilization(item, scen, settings); }
            foreach (var item in mfr.Transmissions) { InitTechUtilization(item, scen, settings); }
        }

        /// <summary>
        /// Initializes the availability and utilization of all applicable technologies for a specified
        /// <see cref="IOEMProduct"/>.
        /// </summary>
        /// <param name="product">
        ///     The <see cref="IOEMProduct"/> whose technology availability and utilization to initialize.
        /// </param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        public static void InitTechUtilization(IOEMProduct product, Scenario scen, ModelingSettings settings)
        {
            int[] techList = product.TechnologyList;
            var techUtilization = product.TechUtilization;
            var techKey = product.BaselineInfo.TechKey;

            foreach (int techIndex in techList) {
                bool isUsed = techKey[techIndex];
                bool isAvailable = CalcAvailability(product, scen, settings, isUsed, techIndex,
                                                    out var yearsAvailable);

                techUtilization[techIndex] = new TechnologyUtilization(isUsed, !isAvailable, yearsAvailable);
            }

            //------------------------------------------------------------------------------------
            // After all technologies are initialized, we need to check those set as used and
            // resolve disable/supersede dependencies. That is, if a technology is used, any
            // "child" technology on the parent's "disables" list must be set disabled. Any
            // "child" on the parent's "supersedes" list must be set disabled if not used, or
            // superseded if already used.
            // 
            // We need to do this to ensure a "clean" and consistent initial state of technology
            // utilization, in case the user didn't define use states correctly in the input.
            //------------------------------------------------------------------------------------
            foreach (int techIndex in techList) {
                ResolveInitializationDependencies(product, techIndex);
            }
        }

        /// <summary>
        /// Refreshes the availability and utilization of a technology with the given index for a specified
        /// <see cref="IOEMProduct"/>.
        /// </summary>
        /// <param name="product">
        ///     The <see cref="IOEMProduct"/> whose technology availability and utilization to refresh.
        /// </param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="analysisYear">The current model year being analyzed.</param>
        /// <param name="applicationYear">
        ///     The model year when the technology was initially installed on the vehicle.
        /// </param>
        /// <param name="techIndex">
        ///     The index of a technology for which to refresh the availability and utilization.
        /// </param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        /// <remarks>
        /// The availability and utilization of technologies should be refreshed after it is installed on a vehicle or
        /// a vehicle component in order to ensure that certain technologies are either set as superseded or disabled.
        /// In the case of a multiyear solution, the technology usage should be refreshed during each affected model
        /// year, by specifying an <paramref name="applicationYear"/> when a technology was initially installed on the
        /// vehicle.
        /// </remarks>
        public static void RefreshTechUtilization(
            IOEMProduct product, Scenario scen, int analysisYear, int applicationYear, int techIndex,
            ModelingSettings settings)
        {
            if (TI.IsConversionToBevOrFcv(techIndex) && product is IVehicle veh) {
                var vehRevision = veh.GetRevision(applicationYear);

                if (vehRevision.RegClass.IsLD()) {
                    ModelYearKey standardSettingYears = scen.GetStandardSettingYears(vehRevision.RegClass);

                    foreach (var tech in product.TechUtilization) {
                        tech.SetUnavailable(standardSettingYears);
                    }
                }
            }

            // resolve disable/supersede dependencies
            for (int year = applicationYear; year <= analysisYear; year++) {
                ResolveDependencies(product, year, techIndex);
            }
        }

        private static bool CalcAvailability(
            IOEMProduct product, Scenario scen, ModelingSettings settings, bool isUsed, int techIndex,
            out ModelYearKey yearsAvailable)
        {
            if (product is IVehicle veh) {
                return CalcAvailability(veh, scen, settings, isUsed, techIndex, out yearsAvailable);
            }
            else if (product is IOEMComponent c) {
                return CalcAvailability(c, scen, settings, isUsed, techIndex, out yearsAvailable);
            }
            else {
                yearsAvailable = ModelYearKey.Empty;
                return false;
            }
        }

        private static bool CalcAvailability(
            IOEMComponent component, Scenario scen, ModelingSettings settings, bool isUsed, int techIndex,
            out ModelYearKey yearsAvailable)
        {
            var techInfo = settings.Technologies.TechnologyList[techIndex];
            var componentBaseInfo = component.BaselineInfo;

            bool isAvailable = !isUsed
                            && !TI.IsBaselineOnly(techIndex)
                            && techInfo.IsApplicableToAny
                            && componentBaseInfo.AvailableTechnologies[techIndex];

            if (!isAvailable) {
                yearsAvailable = ModelYearKey.Empty;
                return false;
            }

            yearsAvailable = CalcYearsAvailable(
                scen, settings.RuntimeSettings, techIndex, techInfo.MinYearAvailable, techInfo.MaxYearRetired,
                componentBaseInfo.RedesignYears, componentBaseInfo.RefreshYears,
                isVehicle: false, vehBaseInfo: null);

            return true;
        }

        private static bool CalcAvailability(
            IVehicle veh, Scenario scen, ModelingSettings settings, bool isUsed, int techIndex,
            out ModelYearKey yearsAvailable)
        {
            bool excludeBevFcv = settings.RuntimeSettings.ExcludeBevFcvFromAnalysis;
            if (excludeBevFcv && TI.IsConversionToBevOrFcv(techIndex)) {
                yearsAvailable = ModelYearKey.Empty;
                return false;
            }

            var vehBaseInfo = veh.BaselineInfo;
            var techInfo = settings.Technologies.TechnologyList[techIndex];
            var techAttr = techInfo.GetAttributes(vehBaseInfo);

            bool isAvailable = !isUsed
                            && !TI.IsBaselineOnly(techIndex)
                            && techAttr.Applicable
                            && vehBaseInfo.AvailableTechnologiesFull[techIndex];

            if (!isAvailable) {
                yearsAvailable = ModelYearKey.Empty;
                return false;
            }

            yearsAvailable = CalcYearsAvailable(
                scen, settings.RuntimeSettings, techIndex, techAttr.YearAvailable, techAttr.YearRetired,
                vehBaseInfo.RedesignYears, vehBaseInfo.RefreshYears, isVehicle: true, vehBaseInfo);

            //------------------------------------------------------------------------------------
            // During standard-setting years, PHEV/BEV/FCV technologies may not be considered for
            // analysis on light-duty vehicles. Hence, we need to disable them. However, we still
            // want to consider these technologies if the vehicle is a ZEV candidate or if it is
            // regulated as HDPUV (non light-duty).
            //------------------------------------------------------------------------------------
            bool isApplicableInStandardSettingYear =
                TechEvaluator.IsTechApplicableInStandardSettingYear(techIndex, vehBaseInfo, settings);

            if (!isApplicableInStandardSettingYear) {
                yearsAvailable &= ~scen.GetStandardSettingYears(vehBaseInfo.RegClass);
            }

            return true;
        }

        /// <summary>
        /// Determine years-available based on the product's refresh/redesign schedules, the technology's years of
        /// availability, and the user selected runtime settings. We follow a slightly different approach when
        /// determining years-available for vehicles versus components. 
        /// <br/><br/>
        /// For vehicles, we make the following assumptions:<br/>
        ///  (1) Engine and transmission-level technologies may be applied during a vehicle's refresh or redesign.<br/>
        ///  (2) All other technologies (platform and vehicle-level) may be applied during a vehicle's refresh or
        ///      redesign (for refresh-based technologies) or during a redesign only (for redesign-based technologies).
        /// <br/><br/>
        /// For components, we make the following assumptions:<br/>
        ///  (1) All technologies may be applied during a component's refresh or redesign (for refresh-based
        ///      technologies) or during a redesign only (for redesign-based technologies).
        /// </summary>
        private static ModelYearKey CalcYearsAvailable(
            Scenario scen, RuntimeSettings runtimeSettings, int techIndex, int techYearAvailable, int techYearRetired,
            ModelYearKey redesignYears, ModelYearKey refreshYears, bool isVehicle, BaselineVehicle vehBaseInfo)
        {
            var yearsAvailable = ModelYearKey.Empty;
            if (TI.IsTiedToRefresh(techIndex) || (isVehicle && TI.IsEngineOrTransmissionLevel(techIndex))) {
                yearsAvailable = redesignYears | refreshYears;
            }
            else if (TI.IsTiedToRedesign(techIndex)) {
                yearsAvailable = redesignYears;
            }

            // for vehicles that are identified as ZEV candidates, we want to force availability
            // of an associated ZEV technology during any year on or after the ZEV application year
            if (isVehicle && vehBaseInfo.IsZevCandidate && techIndex == vehBaseInfo.ZEVCandidate.Value) {
                yearsAvailable.SetRange(startYear: vehBaseInfo.ZevApplicationYear, state: true);
            }

            // adjust years-available range by tech's year-available and year-retired
            yearsAvailable.SetRange(endYear: techYearAvailable - 1, state: false);
            if (techYearRetired != 0) {
                yearsAvailable.SetRange(startYear: techYearRetired + 1, state: false);
            }

            // exclude availability for all years before the first year when we can apply new tech
            // this may vary between baseline and alt scenarios
            int startYear = runtimeSettings.TechStartYear;
            if (!scen.IsBaseline && startYear < runtimeSettings.AlternativeStartYear) {
                startYear = runtimeSettings.AlternativeStartYear;
            }
            yearsAvailable.SetRange(endYear: startYear - 1, state: false);

            return yearsAvailable;
        }

        private static void ResolveInitializationDependencies(IOEMProduct product, int techIndex)
        {
            // when resolving dependencies as part of tech-use initialization, we want to set the
            // year param as "SetInBaseline" --
            // this way, all usage checks and assignments will work on the baseline state
            int year = TechnologyUtilization.SetInBaseline;
            ResolveDependencies(product, year, techIndex);
        }

        private static void ResolveDependencies(IOEMProduct product, int year, int techIndex)
        {
            if (product == null) {
                return;
            }

            var allTechUtilization = product.TechUtilization;
            var techUse = allTechUtilization[techIndex];
            if (!techUse.IsUsed(year)) {
                return;
            }

            // get technologies disabled and superseded by the current tech
            TechKey techDisables = TI.GetTechDisables(techIndex);
            TechKey techSupersedes = TI.GetTechSupersedes(techIndex);
            if (techDisables.IsEmpty && techSupersedes.IsEmpty) {
                return;
            }

            int[] techList = product.TechnologyList;
            foreach (int childTechIndex in techList) {
                var childTechUse = allTechUtilization[childTechIndex];

                if (!childTechUse.IsUsed(year)) {
                    if (techDisables.Contains(childTechIndex) || techSupersedes.Contains(childTechIndex)) {
                        //------------------------------------------------------------------------
                        // When resolving dependencies as part of tech-use initialization, we want
                        // to disable a child tech; otherwise, as part of refreshing after tech
                        // install, we simply make it unavailable. The difference is that disabled
                        // technologies cannot be applied anymore during any year; meanwhile
                        // unavailable technologies could potentially be re-enabled and applied as
                        // part of inheriting, forced use, multiyear, etc.
                        //
                        // TODO: In ResolveDependencies, test/verify disabled vs unavailable behavior
                        //------------------------------------------------------------------------
                        if (year == TechnologyUtilization.SetInBaseline) {
                            childTechUse.SetDisabled();
                        }
                        else {
                            childTechUse.SetUnavailable();
                        }
                    }
                }
                else if (techSupersedes.Contains(childTechIndex) && !childTechUse.IsSuperseded(year)) {
                    childTechUse.SetSuperseded(year);
                }
            }
        }

    }
}
