#region <<< Using Directives >>>
using System;
using System.Linq;
using Volpe.Cafe.MarketData;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.TechAnalysis;
using Volpe.Cafe.VehicleSimulation;

using TechKey = Volpe.Cafe.TechKeys.TechKey128;
using TechPathways = Volpe.Cafe.TechnologyIndexes.TechnologyPathways;
using TI = Volpe.Cafe.TechnologyIndexes;
#endregion

namespace Volpe.Cafe.Model
{
    /// <summary>
    /// Provides static methods to assist the compliance modeling process in initializing and updating technology applicability.
    /// </summary>
    public sealed class TechnologyApplicability
    {
        // we use MR tech indexes a bit for interal calcs, so precache here to reduce number of queries
        // this is a hack and will need to change during broader refactoring of this entire class
        private static int[] MRTechIndexes =
            Enumerable.Range(0, TI.TechnologyCount).Where(index => TI.IsMassReductionPath(index)).ToArray();


        #region < Technology Max Path Utilization >

        /// <summary>
        /// Returns the index of the technology corresponding to the highest level of technology utilization for a
        /// vehicle with the specified techKey on one or more of the specified technology pathways.
        /// If the techKey does not include any technology from any of the specified pathways, this method return -1.
        /// </summary>
        static int GetMaxTechUtilizationIndex(TechKey techKey, TechPathways paths)
        {
            for (int i = TI.TechnologyCount - 1; i >= 0; i--) {
                TechPathways techPath = TI.GetTechPathway(i);
                if ((techPath & paths) == techPath && techKey[i]) {
                    return i;
                }
            }
            return -1;
        }

        /// <summary>
        /// Returns the highest level of Hybrid/Electric path utilization for a vehicle with the specified techKey.
        /// If the techKey does not include any HEV technology, this method return -1.
        /// </summary>
        internal static int GetMaxHEV(TechKey techKey)
        {
            for (int i = TI.TechnologyCount - 1; i >= 0; i--) {
                if (TI.IsHybridElectricPathsCollection(i) && techKey[i]) {
                    return i;
                }
            }
            return -1;
        }

        #endregion

        #region < Technology "Effect" Calculations >

        /// <summary>
        /// Calculates the effect of applying the specified technology on the given vehicle.
        /// </summary>
        /// <param name="scen">The scenario being evaluated by the model.</param>
        /// <param name="year">The model year being evaluated by the model.</param>
        /// <param name="veh">The vehicle for which to calculate the technology effect.</param>
        /// <param name="tech">The technology for which to calculate the technology effect.</param>
        /// <param name="settings">The modeling settings used by the compliance model.</param>
        /// <returns>The effect of applying the specified technology on the given vehicle.</returns>
        public static TechEffect GetTechEffect(
            Scenario scen, int year, IVehicle veh, TechnologyInfo tech, ModelingSettings settings)
        {
            var vehRevision = veh.GetRevision(year);
            TechKey keyRef = vehRevision.TechKey;
            TechKey keyNew = KeyInitializer.TestTechKey(vehRevision, tech.Index);
            if (!SimulationDatabase.TryGetValue(keyRef, out var simRef) ||
                !SimulationDatabase.TryGetValue(keyNew, out var simNew))
            {
                return new TechEffect { IsValid = false };
            }

            // calculate the effect of applying the requested technology on given vehicle
            Technologies techData = settings.Technologies;

            // for mass reduction techs, begin by calculating weight effect
            GetWeightChange(tech, vehRevision, scen, year, techData,
                out double deltaCW, out double deltaGVWR, out double deltaGCWR);

            CalcPseudoAbsoluteCosts(keyRef, vehRevision, year, techData,
                out double initNonBatteryCost, out double initBatteryCost, out double initMaintRepairCost);
            CalcPseudoAbsoluteCosts(keyNew, vehRevision, year, techData,
                out double finalNonBatteryCost, out double finalBatteryCost, out double finalMaintRepairCost);

            // populate tech effect ...
            TechEffect effect = new TechEffect
            {
                IsValid         = true,
                // keys
                KeyRef          = keyRef,
                KeyNew          = keyNew,
                // FE/FC
                FE1ImprvRef     = simRef.GetFE1Adjustment(vehRevision.TechnologyClass),
                FE2ImprvRef     = simRef.GetFE2Adjustment(vehRevision.TechnologyClass),
                FE1ImprvNew     = simNew.GetFE1Adjustment(vehRevision.TechnologyClass),
                FE2ImprvNew     = simNew.GetFE2Adjustment(vehRevision.TechnologyClass),
                FS2             = tech.GetAttributes(vehRevision).SecondaryFS,
                // costs
                BatteryCostRef  = initBatteryCost,
                BatteryCostNew  = finalBatteryCost,
                NonBatteryCost  = finalNonBatteryCost - initNonBatteryCost,
                MaintRepairCost = finalMaintRepairCost - initMaintRepairCost,
                // credits
                ZEVCredit       = tech.GetZevCredits(vehRevision, year),
                // weight changes
                DeltaCW         = deltaCW,
                DeltaGVWR       = deltaGVWR,
                DeltaGCWR       = deltaGCWR
            };

            // if no FE2-improvement exists, use FE1-improvement values
            if (effect.FE2ImprvRef == 0) {
                effect.FE2ImprvRef = effect.FE1ImprvRef;
            }
            if (effect.FE2ImprvNew == 0) {
                effect.FE2ImprvNew = effect.FE1ImprvNew;
            }

            return effect;
        }

        //----- battery cost helper methods -----
        static double GetBatteryCost(
            IVehicleRevision vehRevision, int year, Technologies techData, ISimulationResult simResult)
        {
            int batteryTech = GetMaxBatteryTech(simResult.TechKey);
            if (batteryTech == -1) {
                return 0;
            }
            double learningRate = techData.TechnologyList[batteryTech].GetBatteryLearningRate(vehRevision, year);
            return simResult.GetBatteryCost(vehRevision.TechnologyClass) * learningRate;
        }

        /// <summary>
        /// Returns the index of the technology corresponding to the highest level of battery technology utilization
        /// for the specified tech-key.
        /// </summary>
        public static int GetMaxBatteryTech(TechKey key)
        {
            for (int i = 0; i < TI.TechnologyCount; i++)
            {
                if (key.Contains(i) && TI.IsBatteryCostTech(i)) {
                    return i;
                }
            }
            return -1;
        }

        //----- weight change helper methods -----
        /// <summary>
        /// Determines the changes to the vehicle's weight metrics (curb weight, GVWR, GCWR) as a result of applying
        /// the specified technology to the given vehicle.
        /// </summary>
        static void GetWeightChange(
            TechnologyInfo tech, IVehicleRevision vehRevision, Scenario scen, int year, Technologies techData,
            out double deltaCW, out double deltaGVWR, out double deltaGCWR)
        {
            deltaCW = deltaGVWR = deltaGCWR = 0;
            if (!TI.IsMassReductionPath(tech.Index)) {
                return;
            }

            // compute delta CW
            double gliderMR0 = GetGliderMR0(techData, vehRevision);
            deltaCW = tech.GetAttributes(vehRevision).DeltaWeightPCT * gliderMR0;
            int prevTechIndex = GetMaxTechUtilizationIndex(vehRevision.TechKey, TechPathways.MRPath);
            if (prevTechIndex != -1) {
                TechnologyInfo prevTech = techData.TechnologyList[prevTechIndex];
                double prevDeltaCW = prevTech.GetAttributes(vehRevision).DeltaWeightPCT * gliderMR0;
                deltaCW -= prevDeltaCW;
            }

            // update new variables if there is a change in CW
            if (deltaCW != 0) {
                if (vehRevision.RegClass.IsHDPUV()) {
                    var baseVeh = vehRevision.BaselineInfo;
                    double cw = vehRevision.CurbWeight;
                    double gvwr = vehRevision.GVWR;
                    double gcwr = vehRevision.GCWR;
                    double p = 1 - scen[vehRevision.RegClass, year].PayloadReturn;
                    double t = 1 - scen[vehRevision.RegClass, year].TowingReturn;
                    double gvwrFloor = (vehRevision.VehicleClass == VehicleClass.LDT3) ? 10001 : 8501;

                    deltaGVWR =
                        gvwr - Math.Max(gvwrFloor, Math.Min(gvwr - p * deltaCW, (cw - deltaCW) * baseVeh.MaxGVWRToCW));
                    deltaGCWR = gcwr - Math.Min(gcwr - t * deltaGVWR, (gvwr - deltaGVWR) * baseVeh.MaxGCWRToGVWR);
                }
                else if (vehRevision.TechnologyClass == TechnologyClass.Pickup
                         || vehRevision.TechnologyClass == TechnologyClass.PickupHT) {
                    double gvwr = vehRevision.GVWR;
                    deltaGVWR = gvwr - Math.Max(1, gvwr - deltaCW);
                }
            }
        }

        #endregion

        #region < CalcAuxiliaryHEVCosts methods >

        /// <summary>
        /// Calculates auxiliary costs associated with the hybrid/electric portion for all vehicles (wherever applicable).
        /// The auxiliary costs calculated by this method include: absolute cost of the HEV portion, tax credits associated with
        /// a HEV purchase, consumer's willingness to pay for a HEV, and cost of tech burden attributed to an HEV technology.
        /// </summary>
        /// <param name="modelData">The modeling data currently being analyzed with the current scenario and model year.</param>
        /// <param name="scen">The scenario currently being analyzed.</param>
        /// <param name="year">The model year currently being analyzed.</param>
        /// <param name="technologies">An object containing technology attributes, costs, and adjustment factors.</param>
        public static void CalcAuxiliaryHEVCosts(
            IIndustry modelData, Scenario scen, ModelYear year, Technologies technologies)
        {
            foreach (var mfr in modelData.Manufacturers)
            {   // reset mfr data
                var mfrRevision = mfr.GetRevision(year);
                mfrRevision.InitAuxiliaryHevCosts.Clear(item => item.Clear());
                mfrRevision.AuxiliaryHevCosts.Clear(item => item.Clear());

                foreach (var veh in mfr.Vehicles)
                {
                    var vehRevision = veh.GetRevision(year);
                    var vehInitAuxCosts = vehRevision.InitAuxiliaryHevCosts;
                    var vehFinalAuxCosts = vehRevision.AuxiliaryHevCosts;

                    CalcAuxiliaryHEVCosts(vehRevision, scen, year, technologies, true, vehInitAuxCosts); // "initial" veh state
                    CalcAuxiliaryHEVCosts(vehRevision, scen, year, technologies, false, vehFinalAuxCosts); // "final" veh state

                    // sum up to the mfr on an incremental basis (that is, "final" - "init")
                    double sales = vehRevision.Sales;
                    var mfrInitAuxCosts = mfrRevision.InitAuxiliaryHevCosts[vehRevision.RegClass];
                    var mfrFinalAuxCosts = mfrRevision.AuxiliaryHevCosts[vehRevision.RegClass];

                    mfrInitAuxCosts.HevCost          += sales * vehInitAuxCosts.HevCost;
                    mfrInitAuxCosts.TaxCredit        += sales * vehInitAuxCosts.TaxCredit;
                    mfrInitAuxCosts.BatteryTaxCredit += sales * vehInitAuxCosts.BatteryTaxCredit;
                    mfrInitAuxCosts.ConsumerWTP      += sales * vehInitAuxCosts.ConsumerWTP;

                    mfrFinalAuxCosts.HevCost          += sales * vehFinalAuxCosts.HevCost;
                    mfrFinalAuxCosts.TaxCredit        += sales * vehFinalAuxCosts.TaxCredit;
                    mfrFinalAuxCosts.BatteryTaxCredit += sales * vehFinalAuxCosts.BatteryTaxCredit;
                    mfrFinalAuxCosts.ConsumerWTP      += sales * vehFinalAuxCosts.ConsumerWTP;
                }
            }
        }
        static void CalcAuxiliaryHEVCosts(
            IVehicleRevision vehRevision, Scenario scen, ModelYear year, Technologies technologies, bool initVehState,
            AuxiliaryHEVCosts auxCosts)
        {
            int currentHEV = GetMaxHEV(initVehState ? vehRevision.BaselineInfo.TechKey : vehRevision.TechKey);
            int finalHEV = GetMaxHEV(vehRevision.TechKey);
            var techList = technologies.TechnologyList;

            if (currentHEV != -1)
            {
                // get base cost for HEV tech
                auxCosts.HevCost = techList[currentHEV].GetTechCost(vehRevision, year);

                // add in battery cost (using technologies from final veh state, but HEV from **current** veh state)
                TechKey key = vehRevision.TechKey;
                if (finalHEV != currentHEV)
                {
                    key[finalHEV  ] = false;
                    key[currentHEV] = true;
                }
                double lrnRate = techList[currentHEV].GetBatteryLearningRate(vehRevision, year);
                auxCosts.HevCost += SimulationDatabase.GetValue(key).GetBatteryCost(vehRevision.TechnologyClass) * lrnRate;

                // get tax credits based on selected HEV tech
                double finalMsrp = vehRevision.Msrp + vehRevision.ComplianceCosts.RegCost;
                auxCosts.TaxCredit = GetConstrainedTaxCredit(scen, year, currentHEV, vehRevision, finalMsrp, true);

                var scenInfo = scen[vehRevision.RegClass, year];
                auxCosts.BatteryTaxCredit = scenInfo.GetScaledBatteryTaxCredit(currentHEV, true);

                // get consumer willingness to pay 
                auxCosts.ConsumerWTP = techList[currentHEV].GetAttributes(vehRevision).ConsumerWTP;
            }
        }

        #endregion

        #region < CalcLearnedCosts methods >

        /// <summary>
        /// Calculates total learned-down costs for the specified vehicle based on technology application between the start of
        /// compliance modeling and the specified model year.
        /// </summary>
        /// <param name="vehRevision">The revision of the vehicle for which to calculate learned-down costs.</param>
        /// <param name="year">The model year for which to calculate learned-down costs.</param>
        /// <param name="technologies">An object containing technology attributes, costs, and adjustment factors.</param>
        /// <param name="batteryTechCost">
        ///     When this method returns, contains battery-associated learned-down technology costs for the specified 
        ///     vehicle and model year.
        /// </param>
        /// <param name="nonBatteryTechCost">
        ///     When this method returns, contains non-battery-associated learned-down technology costs for the 
        ///     specified vehicle and model year.
        /// </param>
        /// <param name="mntRpCost">
        ///     When this method returns, contains total learned-down maintenance and repair costs for the specified
        ///     vehicle and model year.
        /// </param>
        public static void CalcLearnedCosts(IVehicleRevision vehRevision, int year, Technologies technologies,
            out double batteryTechCost, out double nonBatteryTechCost, out double mntRpCost)
        {
            //----------------------------------------------------------------------------------//
            // All cost values are defined in the inputs as pseudo-absolute over a preceding    //
            // technology (typically, within its path). However, the model needs to compute and //
            // track the total incremental costs accrued by a vehicle over its initial state.   //
            // To do this, we need to compute the total pseudo-absolute costs for the vehicle's //
            // initial and final states, then take the difference between the two.              //
            //----------------------------------------------------------------------------------//
            CalcPseudoAbsoluteCosts(vehRevision.BaselineInfo.TechKey, vehRevision, year, technologies,
                out double initNonBatteryCost, out double initBatteryCost, out double initMntRpCost);
            CalcPseudoAbsoluteCosts(vehRevision.TechKey, vehRevision, year, technologies,
                out double finalNonBatteryCost, out double finalBatteryCost, out double finalMntRpCost);

            batteryTechCost = finalBatteryCost - initBatteryCost;
            nonBatteryTechCost = finalNonBatteryCost - initNonBatteryCost;
            mntRpCost = finalMntRpCost - initMntRpCost;
        }
        static void CalcPseudoAbsoluteCosts(
            TechKey techKey, IVehicleRevision vehRevision, int year, Technologies technologies,
            out double nonBatteryTechCost, out double batteryTechCost, out double maintRepairCost)
        {
            nonBatteryTechCost = 0;
            maintRepairCost = 0;

            for (int i = 0; i < TI.TechnologyCount; i++) {
                if (!techKey[i]) {
                    continue;
                }

                if (TI.IsMassReductionPath(i)) {
                    var (techCost, repairCost) = CalcMassReduction(i);
                    nonBatteryTechCost += techCost;
                    maintRepairCost += repairCost;
                }
                else {
                    var tech = technologies.TechnologyList[i];
                    nonBatteryTechCost += tech.GetTechCost(vehRevision, year);
                    maintRepairCost += tech.GetMaintRepairCost(vehRevision, year);
                    continue;
                }
            }

            // battery costs are applicable to entire tech-key (instead of individual techs)
            batteryTechCost = GetBatteryCost(vehRevision, year, technologies, SimulationDatabase.GetValue(techKey));

            (double, double) CalcMassReduction(int maxMRTechIndex)
            {
                // for mass reduction technologies, cost is specified as "bracketed" and in $/lbs --
                //  - adjust cost by % reduction of "baseline" glider weight
                //  - accumulate costs between MR0 and current MR level
                double gliderMR0 = GetGliderMR0(technologies, vehRevision);
                double prevDeltaW = 0;
                double massTechCost = 0;
                double massRepairCost = 0;

                foreach (int mrTechIndex in MRTechIndexes) {
                    var tech = technologies.TechnologyList[mrTechIndex];
                    var attr = tech.GetAttributes(vehRevision);
                    double deltaW = attr.DeltaWeightPCT;

                    massTechCost += tech.GetTechCost(vehRevision, year) * (deltaW - prevDeltaW);
                    massRepairCost += tech.GetMaintRepairCost(vehRevision, year) * (deltaW - prevDeltaW);
                    prevDeltaW = deltaW;

                    if (mrTechIndex == maxMRTechIndex) {
                        break;
                    }
                }

                return (massTechCost * gliderMR0, massRepairCost);
            }
        }

        #endregion

        internal static double GetGliderMR0(Technologies technologies, IVehicleRevision veh)
        {
            double gliderShare = technologies.GliderShare[veh.TechnologyClass.GetArrayIndex()];
            return veh.BaselineInfo.CurbWeightMR0 * gliderShare;
        }

        /// <summary>
        /// Determines whether the specified technology has reached its phase-in cap, for the specified model year, and exhausts
        /// its applicability for the provided manufacturer if it has.
        /// </summary>
        /// <param name="mfrRevision">
        ///     The modeling data of a manufacturer for which to update the tech-exhausted flags if technology phase-in
        ///     has been exhausted.
        /// </param>
        /// <param name="tech">The technology for which to check the phase-in.</param>
        /// <param name="year">The current model year being analyzed.</param>
        public static void CheckPhaseIn(IManufacturerRevision mfrRevision, TechnologyInfo tech, int year)
        {
            // do not check phase-in for technologies that have already been exhausted
            if (mfrRevision.TechExhausted[tech.Index]) {
                return;
            }

            double techSales = mfrRevision.TechAppliedSales[tech.Index];
            double mfrSales = mfrRevision.ComplianceStats.GetSales().Total;
            if (mfrSales > 0 && techSales >= mfrSales * tech.GetPhaseIn(year)) {
                // exhaust phase-in of the tech
                if (tech.Index != -1) {
                    TechKey techExhausted = mfrRevision.TechExhausted;
                    techExhausted[tech.Index] = true;
                    mfrRevision.TechExhausted = techExhausted;
                }
            }
        }

        /// <summary>
        /// Computes tax credits for a vehicle, subject to MSRP constraints.
        /// This is a temp function -- use this until we get a better (i.e., cleaner) way to do this.
        /// </summary>
        public static double GetConstrainedTaxCredit(
            Scenario scen, int year, int techIndex, IVehicleInfo vehInfo, double testMsrp,
            bool consumerPerspective)
        {
            if (techIndex == -1) {
                return 0;
            }

            // starting in MY-2023, MSRP limit applies:
            //  for SUVs, vans, pickups = $80k max
            //  all other vehicles = $55k max
            double msrpCap = (year < 2023) ? double.MaxValue : vehInfo.Style.IsCar() ? 55000 : 80000;
            if (testMsrp > msrpCap) {
                return 0;
            }

            var scenInfo = scen[vehInfo.RegClass, year];
            return scenInfo.GetScaledTaxCredit(techIndex, consumerPerspective);
        }

    }
}
