#region <<< Using Directives >>>
using System;
using System.Collections.Generic;
using System.Linq;

using Volpe.Cafe.EffectsModels;
using Volpe.Cafe.Generic;
using Volpe.Cafe.ModelSettings;

using VS = Volpe.Cafe.VehicleStyle;
#endregion

namespace Volpe.Cafe.Model
{
    /// <summary>
    /// Provides methods to support the Dynamic Scrappage model.
    /// </summary>
    [Serializable]
    public class ScrappageModel
    {

        #region <<< Nested Types >>>

        /// <summary>
        /// Defines a static scrappage model, that computes survival rates based on a static schedule.
        /// </summary>
        [Serializable]
        class StaticScrappageModel : ScrappageModel
        {
            public StaticScrappageModel(ICompliance complianceModel)
                : base(complianceModel, null, false) { }

            public override VSValue<double[]> GetScrappedFleetByCY(int calendarYear)
            {
                throw new NotSupportedException("The static Scrappage Model does not support " +
                    "calculation or retrieval of scrapped fleet by calendar year.");
            }

            public override VSValue<double[]> GetScrappedFleet(int year)
            {
                var hfd = this.Parameters.HistoricFleetData;
                return hfd.GetAnnualHistoricFleetData(year, this.ComplianceModel.MinYear, this.Parameters.SurvivalRates);
            }

            public override VSValue<double[]> GetSurvivalRates(int year)
            {
                return this.Parameters.SurvivalRates;
            }
        }

        /// <summary>
        /// Defines the static fleet data object to use for describing the state of the fleet for each specific model
        /// year.
        /// </summary>
        [Serializable]
        class StaticFleetData
        {
            //----- Ctors -----
            public StaticFleetData(int year, double initialProduction, double pcShare, FuelValue onRoadFE,
                FuelValue fuelShare, double horsepower, double curbWeight, double transactionPrice)
            {
                this.ModelYear         = year;
                this.InitialProduction = initialProduction;
                this.PCShare           = pcShare;
                this.OnRoadFE          = onRoadFE;
                this.FuelShare         = fuelShare;
                this.Horsepower        = horsepower;
                this.CurbWeight        = curbWeight;
                this.TransactionPrice  = transactionPrice;
            }
            public StaticFleetData(int year, HistoricFleetDataItem historicFleetData)
            {
                this.ModelYear         = year;
                this.InitialProduction = historicFleetData.InitialProduction;
                this.PCShare           = historicFleetData.PCShare;
                this.OnRoadFE          = historicFleetData.OnRoadFE;
                this.FuelShare         = historicFleetData.FuelShare;
                this.Horsepower        = historicFleetData.Horsepower;
                this.CurbWeight        = historicFleetData.CurbWeight;
                this.TransactionPrice  = historicFleetData.TransactionPrice;
            }

            //----- Properties -----
            /// <summary>Gets the model year for which the static fleet data is defined.</summary>
            public int ModelYear { get; }

            /// <summary>Gets the initial production (the on-road fleet at age 0) of a specific model year.</summary>
            public double InitialProduction { get; }
            /// <summary>Gets the share of the on-road fleet that are regulated as passenger cars.</summary>
            public double PCShare { get; }
            /// <summary>Gets the average on-road vehicle fuel economy for a specific model year.</summary>
            public FuelValue OnRoadFE { get; }
            /// <summary>Gets the average vehicle fuel share for a specific model year.</summary>
            public FuelValue FuelShare { get; }
            /// <summary>Gets the average vehicle horsepower for a specific model year.</summary>
            public double Horsepower { get; }
            /// <summary>Gets the average vehicle curb weight for a specific model year.</summary>
            public double CurbWeight { get; }
            /// <summary>Gets the average transaction price of vehicle sales during a specific model year.</summary>
            public double TransactionPrice { get; }
        }

        /// <summary>
        /// Defines the dynamic fleet data object to use for describing the annual state of the fleet during each
        /// specific calendar year and vehicle age.
        /// </summary>
        [Serializable]
        class DynamicFleetData
        {
            //----- Ctors -----
            public DynamicFleetData(int calendarYear, int age, double onRoadFleet)
            {
                this.CalendarYear = calendarYear;
                this.Age          = age;
                this.OnRoadFleet  = onRoadFleet;
            }

            //----- Properties -----
            /// <summary>Gets the calendar year for which the dynamic fleet data is defined.</summary>
            public int CalendarYear { get; }
            /// <summary>Gets the vehicle age for which the dynamic fleet data is defined.</summary>
            public int Age { get; }
            /// <summary>Gets the model year for which the dynamic fleet data is defined.</summary>
            public int ModelYear => this.CalendarYear - this.Age;

            /// <summary>Gets or sets the surviving on-road fleet for vehicles of specific model year and age, during
            ///     the given calendar year.</summary>
            public double OnRoadFleet { get; set; }
        }

        #endregion

        #region <<< Ctors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="ScrappageModel"/>, associated with the specified compliance
        /// model and scenario.
        /// </summary>
        /// <param name="complianceModel">The compliance model associated with this <see cref="ScrappageModel"/>
        ///     instance.</param>
        /// <param name="scen">The scenario for which to obtain the final vehicle data to use for estimating scrappage.</param>
        /// <param name="initDSMData">true, to initialize data required for the dynamic scrappage model; false, otherwise.</param>
        ScrappageModel(ICompliance complianceModel, Scenario scen, bool initDSMData)
        {
            this.ComplianceModel = complianceModel;
            this.Scenario = scen;

            if (initDSMData)
            {
                this.InitializeModelYearFleetData();
                this.InitializeCalendarYearFleetData();
                this.CalculateScrappedFleet();
            }
        }

        #endregion

        #region <<< Methods >>>

        /// <summary>
        /// Initializes and returns a new instance of the dynamic <see cref="ScrappageModel"/>, associated with the
        /// specified compliance model and scenario, which can be used to dynamically estimate the survival rates and
        /// remaining on-road fleet.
        /// </summary>
        /// <param name="complianceModel">The compliance model associated with this <see cref="ScrappageModel"/>
        ///     instance.</param>
        /// <param name="scen">The scenario for which to obtain the final vehicle data to use for estimating scrappage.</param>
        /// <returns>A dynamic <see cref="ScrappageModel"/>.</returns>
        public static ScrappageModel CreateDynamicScrappageModel(ICompliance complianceModel, Scenario scen)
        {
            return new ScrappageModel(complianceModel, scen, true);
        }
        /// <summary>
        /// Initializes and returns a new instance of a legacy scrappage model, associated with the specified compliance
        /// model, which uses static survival rates and fleet.
        /// </summary>
        /// <param name="complianceModel">The compliance model associated with this <see cref="ScrappageModel"/>
        ///     instance.</param>
        /// <returns>A legacy scrappage model, using static survival rates and fleet.</returns>
        public static ScrappageModel CreateStaticScrappageModel(ICompliance complianceModel)
        {
            return new StaticScrappageModel(complianceModel);
        }

        /// <summary>
        /// Returns the scrapped fleet computed by the <see cref="ScrappageModel"/> model for the specified calendar
        /// year, for each vehicle age and style. Index 0 (zero) in each array corresponds to age 0 for the specified
        /// calendar year; the second index corresponds to age 1 for calendarYear - 1; the third index corresponds to
        /// age 2 for calendarYear - 2; and so on.
        /// </summary>
        /// <param name="calendarYear">The calendar year for which to obtain the computed scrapped fleet.</param>
        /// <returns>The scrapped fleet for the specified calendar year, for each vehicle age and style.</returns>
        public virtual VSValue<double[]> GetScrappedFleetByCY(int calendarYear)
        {
            var fleet = new VSValue<double[]>();
            for (int i = 0; i < fleet.Items.Length; i++)
            {
                fleet.Items[i] = new double[Globals.CY];
            }

            // return empty fleet if requested calendar year is less than the lowest analysis year
            if (calendarYear < this.ComplianceModel.MinYear)
                return fleet;

            var fleetDataCY = this._dynamicFleetDataCY[calendarYear];
            foreach (var vehStyle in VehStyles)
            {
                for (int age = 0; age < Globals.CY; age++)
                {
                    fleet[vehStyle][age] = fleetDataCY[vehStyle][age].OnRoadFleet;
                }
            }

            return fleet;
        }
        /// <summary>
        /// Returns the scrapped fleet computed by the <see cref="ScrappageModel"/> model for the specified model year.
        /// </summary>
        /// <param name="year">The model year for which to obtain the computed scrapped fleet.</param>
        /// <returns>The scrapped fleet for the specified model year.</returns>
        public virtual VSValue<double[]> GetScrappedFleet(int year)
        {
            var fleet = new VSValue<double[]>(() => new double[Globals.CY]);

            // return empty fleet if requested model year is less than the lowest supported historic year
            int minHstMY = this.ComplianceModel.MinYear - Globals.CY + 1;
            if (year < minHstMY) {
                return fleet;
            }

            // assume min calendar year is equal to the first analysis year
            int minCY = this.ComplianceModel.MinYear;
            for (int age = minCY - year; age < Globals.CY; age++)
            {
                int calendarYear = year + age;
                var fleetDataCY  = this._dynamicFleetDataCY[calendarYear];
                foreach (var vehStyle in VehStyles)
                {
                    fleet[vehStyle][age] = fleetDataCY[vehStyle][age].OnRoadFleet;
                }
            }

            return fleet;
        }
        /// <summary>
        /// Returns the survival rates computed by the <see cref="ScrappageModel"/> model for the specified model year.
        /// </summary>
        /// <param name="year">The model year for which to obtain the computed survival rates.</param>
        /// <returns>The survival rates for the specified model year.</returns>
        public virtual VSValue<double[]> GetSurvivalRates(int year)
        {
            var surv = new VSValue<double[]>();
            for (int i = 0; i < surv.Items.Length; i++)
            {
                surv.Items[i] = new double[Globals.CY];
            }

            // return empty survival rates if requested model year is less than the lowest supported historic year
            int minHstMY = this.ComplianceModel.MinYear - Globals.CY + 1;
            if (year < minHstMY) {
                return surv;
            }

            foreach (var vehStyle in VehStyles)
            {
                var fleetDataMY = this._staticFleetDataMY[year][vehStyle];
                for (int age = 0; age < Globals.CY; age++)
                {
                    var fleetDataCY = this._dynamicFleetDataCY[year + age][vehStyle];
                    surv[vehStyle][age] = fleetDataCY[age].OnRoadFleet / fleetDataMY.InitialProduction;
                }
            }

            return surv;
        }

        /// <summary>
        /// Initializes model year fleet data for the historic, analysis, and future model years.
        /// </summary>
        void InitializeModelYearFleetData()
        {
            this._staticFleetDataMY = new Dictionary<int, VSValue<StaticFleetData>>();
            this.InitializeHistoricModelYearFleetData();
            this.InitializeAnalysisModelYearFleetData();
        }
        /// <summary>
        /// Initializes model year fleet data for the historic model years, based on the seed inputs defined for the
        /// scrappage model.
        /// </summary>
        void InitializeHistoricModelYearFleetData()
        {
            int minHstMY = this.ComplianceModel.MinYear - Globals.CY + 1;
            int maxHstMY = this.ComplianceModel.MinYear - 1;

            for (int year = minHstMY; year <= maxHstMY; year++)
            {
                var fleetDataMY = new VSValue<StaticFleetData>();
                foreach (var vehStyle in VehStyles)
                {
                    var historicFleetData = this.Parameters.HistoricFleetData.GetHistoricFleetData(vehStyle, year);
                    fleetDataMY[vehStyle] = new StaticFleetData(year, historicFleetData);
                }
                this._staticFleetDataMY.Add(year, fleetDataMY);
            }
        }
        /// <summary>
        /// Initializes model year fleet data for the analysis and future model years, based on the compliance model
        /// data.
        /// </summary>
        void InitializeAnalysisModelYearFleetData()
        {
            int minMY = this.ComplianceModel.MinYear;
            int maxMY = this.ComplianceModel.MaxYear + Globals.CY - 1;

            for (int year = minMY; year <= maxMY; year++)
            {
                //--- calculate fleet-wide averages for the year across the entire industry ---
                var pcShare  = new VSValue<double>();    // pcShare is PCSales/TotalSales
                var sales    = new VSValue<FuelValue>(); // sales <=> InitialProduction
                var feSum    = new VSValue<FuelValue>();
                var fsSum    = new VSValue<FuelValue>();
                var hpSum    = new VSValue<double>();
                var cwSum    = new VSValue<double>();
                var priceSum = new VSValue<double>(); // priceSum <=> TransactionPrice

                // for projected model years, aggregate compliance data using
                // last model year evaluated for compliance
                int complianceYear = Math.Min(year, this.ComplianceModel.MaxYear);

                this.AggregateComplianceData(
                    complianceYear, sales, pcShare, feSum, fsSum, hpSum, cwSum, priceSum);

                //--- save static fleet data for the year ---
                var fleetDataMY = new VSValue<StaticFleetData>();
                foreach (var vehStyle in VehStyles)
                {
                    fleetDataMY[vehStyle] = new StaticFleetData(year, sales[vehStyle].Total, pcShare[vehStyle],
                        feSum[vehStyle], fsSum[vehStyle], hpSum[vehStyle], cwSum[vehStyle], priceSum[vehStyle]);
                }
                this._staticFleetDataMY.Add(year, fleetDataMY);
            }
        }
        void AggregateComplianceData(int year,
            VSValue<FuelValue> sales, VSValue<double> pcShare, VSValue<FuelValue> feSum, VSValue<FuelValue> fsSum,
            VSValue<double> hpSum, VSValue<double> cwSum, VSValue<double> priceSum)
        {
            var data = this.ComplianceModel.GetData(this.Scenario);
            var econParams = this.Parameters.EconomicValues.Parameters;

            foreach (var mfr in data.Manufacturers)
            {
                foreach (var veh in mfr.Vehicles)
                {
                    var vehRevision = veh.GetRevision(year);
                    double vehSales = vehRevision.Sales;
                    var vehStyle = vehRevision.Style;
                    int vsIndex = VSValue<object>.GetIndex(vehStyle);

                    if (vehRevision.RegClass.IsPC())
                    {   // for vehicles regulated as PC, we wish to accrue sales separately
                        // in order to calculate PC-share down below
                        pcShare.Items[vsIndex] += vehSales;
                    }

                    var fuelShare = Effects.GetModifiedVehicleFS(this.Scenario, year, vehRevision);
                    var fuelEconomy = Effects.CalcRatedVehicleFE(this.Scenario, year, vehRevision);
                    var onRoadGap = econParams[vehRevision.Style].OnRoadGap;

                    foreach (FuelType fuel in FuelTypes.List)
                    {
                        if (fuelShare[fuel] != 0)
                        {
                            double vehSalesFS = vehSales * fuelShare[fuel];
                            feSum.Items[vsIndex][fuel] += vehSalesFS / (fuelEconomy[fuel] * (1 - onRoadGap[fuel]));
                            fsSum.Items[vsIndex][fuel] += vehSalesFS;
                            sales.Items[vsIndex][fuel] += vehSalesFS;
                        }
                    }

                    hpSum.Items[vsIndex] += vehSales * veh.BaselineInfo.VehiclePower;
                    cwSum.Items[vsIndex] += vehSales * vehRevision.CurbWeight;

                    bool applyTaxCredits = this.Scenario[vehRevision.RegClass, year].ApplyTaxCredits;
                    double taxCredit = (applyTaxCredits)
                        ? vehRevision.AuxiliaryHevCosts.TaxCredit - vehRevision.InitAuxiliaryHevCosts.TaxCredit
                        : 0;
                    bool applyBatteryTaxCredits = this.Scenario[vehRevision.RegClass, year].ApplyBatteryTaxCredits;
                    double batteryTaxCredit = (applyBatteryTaxCredits)
                        ? vehRevision.AuxiliaryHevCosts.BatteryTaxCredit - vehRevision.InitAuxiliaryHevCosts.BatteryTaxCredit
                        : 0;
                    double regCost = vehRevision.ComplianceCosts.RegCost;
                    priceSum.Items[vsIndex] += vehSales * (regCost - taxCredit - batteryTaxCredit);
                }
            }

            // finalize averages
            for (int i = 0; i < sales.Items.Length; i++)
            {
                double vsSalesTotal = sales.Items[i].Total;
                if (vsSalesTotal > 0)
                {
                    foreach (FuelType fuel in FuelTypes.List)
                    {
                        if (sales.Items[i][fuel] != 0)
                        {
                            feSum.Items[i][fuel] = sales.Items[i][fuel] / feSum.Items[i][fuel];
                            fsSum.Items[i][fuel] = fsSum.Items[i][fuel] / vsSalesTotal;
                        }
                    }
                    hpSum  .Items[i] /= vsSalesTotal;
                    cwSum  .Items[i] /= vsSalesTotal;
                    pcShare.Items[i] /= vsSalesTotal;
                }
            }

            // finalize average transaction price --
            //  - transaction price should be averaged over entire industry
            //    (separated by LD vs 2b3 fleets), rather than by individual
            //    veh-style, given the way the scrappage model was benchmarked
            //  - additionally, seed price from the last historic model year
            //    should be added to the new price
            FinalizeTransactionPrice(VehStylesLD);
            FinalizeTransactionPrice(VehStyles2b3);

            void FinalizeTransactionPrice(VS[] vehStyles)
            {
                double avgPrice = vehStyles.Sum(vehStyle => priceSum[vehStyle])
                                / vehStyles.Sum(vehStyle => sales[vehStyle].Total);

                int maxHstMY = this.ComplianceModel.MinYear - 1;
                var hfd = this.Parameters.HistoricFleetData;

                foreach (var vehStyle in vehStyles) {
                    double transactionPrice = hfd.GetHistoricFleetData(vehStyle, maxHstMY).TransactionPrice;
                    priceSum[vehStyle] = transactionPrice + avgPrice;
                }
            }
        }

        /// <summary>
        /// Initializes calendar year fleet data for the historic model years, based on the seed inputs defined for the
        /// scrappage model. The seed inputs correspond to the state of the on-road fleet during the calendar year that
        /// equates to the first model year evaluated for compliance.
        /// </summary>
        void InitializeCalendarYearFleetData()
        {
            this._dynamicFleetDataCY = new Dictionary<int, VSValue<DynamicFleetData[]>>();

            int calendarYear = this.ComplianceModel.MinYear;
            var fleetDataCY  = new VSValue<DynamicFleetData[]>();

            foreach (var vehStyle in VehStyles)
            {
                fleetDataCY[vehStyle] = new DynamicFleetData[Globals.CY];

                // initialize calendar year fleet data for age-0, where CY=MY -->
                //  - for age-0, the lag terms need to be initialized to -10,
                //    this approximates a 0% scrappage rate on a logit curve
                //  - for age-0, on-road-fleet is the same as initial production,
                //    since none of it has been scrapped yet
                double onRoadFleet = this._staticFleetDataMY[calendarYear][vehStyle].InitialProduction;
                var age0 = new DynamicFleetData(calendarYear, 0, onRoadFleet);
                fleetDataCY[vehStyle][0] = age0;

                // initialize calendar year fleet data for ages 1 to 39 -->
                //  - each subsequent age corresponds to on-road fleet data and
                //    coefficients from a previous model year
                for (int age = 1; age < Globals.CY; age++)
                {
                    var hfd = this.Parameters.HistoricFleetData.GetHistoricFleetData(vehStyle, calendarYear - age);
                    var ageN = new DynamicFleetData(calendarYear, age, hfd.OnRoadFleet);
                    fleetDataCY[vehStyle][age] = ageN;
                }
            }
            this._dynamicFleetDataCY.Add(calendarYear, fleetDataCY);
        }

        void CalculateScrappedFleet()
        {
            // starting with the CY equal to the first MY evaluated for compliance,
            // project on-road fleet (or, population) for each CY
            // each CY contains 40 ages worth of model years, where in age 0 MY=CY
            // and in age 39 MY=CY-39
            int minCY = this.ComplianceModel.MinYear;
            int maxCY = this.ComplianceModel.MaxYear + Globals.CY - 1;

            for (int year = minCY; year < maxCY; year++)
            {
                var fleetDataCY = new VSValue<DynamicFleetData[]>();
                foreach (var vehStyle in VehStyles)
                {
                    fleetDataCY[vehStyle] = this.CalculateScrappedFleet(vehStyle, year);
                }
                this._dynamicFleetDataCY.Add(year + 1, fleetDataCY);
            }
        }
        DynamicFleetData[] CalculateScrappedFleet(VS vehStyle, int calendarYear)
        {
            var newFleetData = new DynamicFleetData[Globals.CY];
            int newCY = calendarYear + 1;

            // initialize new fleet data for age-0
            double onRoadFleet = this._staticFleetDataMY[newCY][vehStyle].InitialProduction;
            newFleetData[0] = new DynamicFleetData(newCY, 0, onRoadFleet);

            // calculate new fleet data for ages 1 to 39 -->
            // use current calendar year population from the previous age to compute new
            // calendar year population for each subsequent age
            for (int age = 1; age < Globals.CY; age++)
            {
                double sr = this.CalculateScrappageRate(vehStyle, calendarYear, age - 1);

                // initialize new fleet data for the new CY and MY/age --
                //  - get calendar year fleet data corresponding to the model year that
                //    was just scrapped for the preceding age
                //  - apply scrappage rate to the on-road fleet of the previous age to
                //    obtain new on-road fleet surviving into the next age during the
                //    same model year
                var fleetDataCYAge = this.GetDynamicFleetDataCY(calendarYear, vehStyle, age - 1);
                newFleetData[age] = new DynamicFleetData(newCY, age, fleetDataCYAge.OnRoadFleet * (1 - sr)); // new on-road fleet
            }

            return newFleetData;
        }
        double CalculateScrappageRate(VS vehStyle, int calendarYear, int age)
        {
            // get calendar year fleet data corresponding to the model year we wish to scrap for the current age
            var fleetDataCYAge = this.GetDynamicFleetDataCY(calendarYear, vehStyle, age);
            // additionally, get static model year fleet data for model year being scrapped
            var fleetDataMYAge = this.GetStaticFleetDataMY(fleetDataCYAge.ModelYear, vehStyle);

            var      macroParams = this.Parameters.EconomicValues.MacroParameters;
            double[] beta        = this.Parameters.ScrappageModelValues.BetaCoefficients[vehStyle];
            int      decayStart  = (int)beta[15];
            double   finalSurv   = beta[16];
            double   surv        = fleetDataCYAge.OnRoadFleet / fleetDataMYAge.InitialProduction; // surviving fleet MY and age
            double   sr          = 0;

            if (surv < finalSurv || age < decayStart)
            {
                // get model year fleet data where model year = calendar year, as well as a preceding model year
                var fleetDataMY     = this.GetStaticFleetDataMY(calendarYear    , vehStyle);
                var fleetDataMYLag1 = this.GetStaticFleetDataMY(calendarYear - 1, vehStyle);

                // pre-calc variables to use for estimating the covariant vector
                int    ageSqr         = age * age;
                int    ageCube        = ageSqr * age;
                int    modelYear      = fleetDataCYAge.ModelYear;
                double vehPriceMY     = fleetDataMY    .TransactionPrice; // price is fleet-wide average
                double vehPriceLag1   = fleetDataMYLag1.TransactionPrice;
                double fuelSavingsMY  = this.CalculateFleetWideFuelSavings(calendarYear    , !vehStyle.Is2b3());
                double fuelSavingsLag = this.CalculateFleetWideFuelSavings(calendarYear - 1, !vehStyle.Is2b3());
                double cpm            = this.CalculateCPMCents            (calendarYear    , fleetDataMYAge);
                double cpmLag         = this.CalculateCPMCents            (calendarYear - 1, fleetDataMYAge);
                double fuelPrice      = this.CalculateRetailFuelPrice     (calendarYear    , fleetDataMYAge);
                double fuelPriceLag   = this.CalculateRetailFuelPrice     (calendarYear - 1, fleetDataMYAge);
                double gdpGrowthRate  = macroParams.GetGdpGrowthRate      (calendarYear);

                double cv =
                    // age polynomial
                    (beta[0] * age + beta[1] * ageSqr + beta[2] * ageCube) +
                    // on-road surviving fleet polynomial
                    (beta[3] + beta[4] * age) * surv +
                    // price-vs-fuel-savings polynomial
                    (beta[5] + beta[6] * age + beta[7] * ageSqr + beta[8] * ageCube) *
                        (vehPriceMY - fuelSavingsMY - vehPriceLag1 + fuelSavingsLag) +
                    // fuel price and CPM differences
                    beta[ 9] * (fuelPrice - fuelPriceLag) +
                    beta[10] * (cpm - cpmLag) +
                    // GDP growth
                    beta[11] * gdpGrowthRate * 100 +
                    // intercept
                    beta[12] +
                    // model year durability
                    beta[13] * Math.Min(modelYear, beta[14]);

                double exp = Math.Exp(cv);
                sr = exp / (1 + exp);
            }
            else
            {   // switch to a decay function past a certain age
                sr = 1 - Math.Exp(Math.Log(finalSurv / surv) / (Globals.CY - 1 - age));
            }

            return sr;
        }
        double CalculateFleetWideFuelSavings(int calendarYear, bool isLightDuty)
        {
            //------------------------------------------------------------------------------------
            // Calculate fuel savings as the difference in fuel cost-per-mile (CPM), between
            // historic vehicles from 1975 and vehicles from the current calendar year, multiplied
            // by the assumed number of miles during which an added investment in fuel improving
            // technology is expected to pay back.
            // We wish to compute fleet-wide fuel savings (PC+LT), but we still need to keep
            // light-duty and 2b/3 vehicles separate.
            //------------------------------------------------------------------------------------

            FuelValue refFE = FuelValue.Zero, refFS = FuelValue.Zero;
            FuelValue vehFE = FuelValue.Zero, vehFS = FuelValue.Zero;

            var desiredStyles = (isLightDuty) ? VehStylesLD : VehStyles2b3;
            foreach (var vehStyle in desiredStyles)
            {
                var fleetDataHst = this.Parameters.HistoricFleetData.GetHistoricFleetData(vehStyle, 1975);
                var fleetDataMY  = this.GetStaticFleetDataMY(calendarYear, vehStyle);

                FuelValue refSaleFS = fleetDataHst.InitialProduction * fleetDataHst.FuelShare;
                FuelValue vehSaleFS = fleetDataMY.InitialProduction * fleetDataMY.FuelShare;

                refFE += FuelValue.SafeDivide(refSaleFS, fleetDataHst.OnRoadFE);
                refFS += refSaleFS;
                vehFE += FuelValue.SafeDivide(vehSaleFS, fleetDataMY.OnRoadFE);
                vehFS += vehSaleFS;
            }

            refFE  = FuelValue.SafeDivide(refFS, refFE);
            refFS /= refFS.Total;
            vehFE  = FuelValue.SafeDivide(vehFS, vehFE);
            vehFS /= vehFS.Total;

            return Effects.CalcAssumedFuelSavings(calendarYear, refFE, refFS, vehFE, vehFS, this.Settings);
        }
        double CalculateCPMCents(int calendarYear, StaticFleetData fleetDataMY)
        {
            return Effects.CalcCPM(calendarYear, fleetDataMY.OnRoadFE, fleetDataMY.FuelShare, this.Settings) * 100;
        }
        double CalculateRetailFuelPrice(int calendarYear, StaticFleetData fleetDataMY)
        {
            var    fuelPrices     = this.Parameters.FuelPrices;
            var    fuelProperties = this.Parameters.FuelProperties;
            var    fuelShare      = fleetDataMY.FuelShare;
            double fuelPrice      = 0D;

            var validFuelTypes = FuelTypes.List.Where(fuelType => fuelShare[fuelType] != 0);
            foreach (FuelType fuelType in validFuelTypes)
            {
                double fp = fuelPrices.GetFuelPrice(fuelType, calendarYear, fuelProperties);
                fuelPrice += (fp * fuelShare[fuelType]);
            }

            return fuelPrice;
        }

        StaticFleetData GetStaticFleetDataMY(int year, VS vehStyle)
        {
            return this._staticFleetDataMY[year][vehStyle];
        }
        DynamicFleetData GetDynamicFleetDataCY(int year, VS vehStyle, int age)
        {
            return this._dynamicFleetDataCY[year][vehStyle][age];
        }

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets the compliance model associated with this <see cref="ScrappageModel"/> instance.</summary>
        protected ICompliance ComplianceModel { get; }
        /// <summary>Gets the scenario associated with this <see cref="ScrappageModel"/> instance.</summary>
        protected Scenario Scenario { get; }

        /// <summary>Gets the modeling settings used by the <see cref="ComplianceModel"/>.</summary>
        protected ModelingSettings Settings => this.ComplianceModel.Settings;
        /// <summary>Gets the parameters used by the <see cref="ComplianceModel"/>.</summary>
        protected Parameters Parameters => this.Settings.Parameters;

        #endregion

        #region <<< Variables >>>

        //----- static variables -----
        /// <summary>Specifies the <see cref="VS"/>s supported by the Scrappage Model. This field is read-only.</summary>
        private static readonly VS[] VehStyles = { VS.GenericCar,
                                                   VS.GenericVanSUV,
                                                   VS.GenericPickup,
                                                   VS.Generic2b3 };

        private static readonly VS[] VehStylesLD =
            VehicleStyles.AltList.Where(vehStyle => vehStyle != VS.Generic2b3).ToArray();
        private static readonly VS[] VehStyles2b3 =
            VehicleStyles.AltList.Where(vehStyle => vehStyle == VS.Generic2b3).ToArray();

        //----- runtime variables -----
        Dictionary<int, VSValue<StaticFleetData>> _staticFleetDataMY;     // model year (ex: 1978-2089)
        Dictionary<int, VSValue<DynamicFleetData[]>> _dynamicFleetDataCY; // calendar year (ex: 2017-2089), age(0-39)

        #endregion

    }
}
