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

using Volpe.Cafe.EffectsData;
using Volpe.Cafe.EffectsData.Collections;
using Volpe.Cafe.EffectsModels;
using Volpe.Cafe.Generic;
using Volpe.Cafe.MarketData;
using Volpe.Cafe.MarketData.BaselineInfo;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.Model;
using Volpe.Cafe.ModelSettings;

using FT = Volpe.Cafe.FuelType;
using RC = Volpe.Cafe.RegulatoryClass;
using SC = Volpe.Cafe.SafetyClass;
using VC = Volpe.Cafe.VehicleClass;
using VS = Volpe.Cafe.VehicleStyle;
#endregion

namespace Volpe.Cafe.EffectsModel
{
    /// <summary>
    /// Provides methods for calculating modeling effects.
    /// </summary>
    [Serializable]
    public class EffectsModel
    {

        #region <<< Nested Types >>>

        //
        // Contains a snapshot of some vehicle-level effects from the baseline scenario required
        // for incremental fatality-related calculations in alternative scenarios.
        //
        class BaselineVehicleEffects
        {
            public BaselineVehicleEffects(VehicleEffects vehEff)
            {
                this.Sales          = vehEff.Sales;
                this.CurbWeight     = vehEff.CurbWeight;
                this.FS             = vehEff.FS;
                this.Fleet          = new FuelValue[Globals.CY];
                this.VMT            = new FuelValue[Globals.CY];
                this.VMTNoRebound   = new FuelValue[Globals.CY];
                this.VMTReboundOnly = new FuelValue[Globals.CY];
                this.VMTReallocated = new FuelValue[Globals.CY];
            }

            public void SaveAgeAndFuelTypeInfo(int age, FT fuelType, VehicleEffects vehEff)
            {
                this.Fleet         [age][fuelType] = vehEff.Fleet;
                this.VMT           [age][fuelType] = vehEff.VMT;
                this.VMTNoRebound  [age][fuelType] = vehEff.VMTNoRebound;
                this.VMTReboundOnly[age][fuelType] = vehEff.VMTReboundOnly;
                this.VMTReallocated[age][fuelType] = vehEff.VMTReallocated;
            }

            public double Sales;
            public double CurbWeight;
            public FuelValue FS;
            public FuelValue[] Fleet;
            public FuelValue[] VMT;
            public FuelValue[] VMTNoRebound;
            public FuelValue[] VMTReboundOnly;
            public FuelValue[] VMTReallocated;
        }

        //
        // Contains vehicle-level effects per age and FT
        // (Consumer costs are accumulated across FT per age)
        //
        class VehicleEffects
        {
            public VehicleEffects(int societalDRCount = 0, int consumerDRCount = 0)
            {
                this.UpstreamEHI = new double[HealthImpacts.List.Length];
                this.TailpipeEHI = new double[HealthImpacts.List.Length];

                this.Upstream = new UpstreamEmissionsValue();
                this.Tailpipe = new EmissionsValue();
                this.OwnershipCosts = new OwnershipCosts();
                this.EmCosts = new EmissionsCosts();

                this.InitUpstream = new UpstreamEmissionsValue();
                this.InitTailpipe = new EmissionsValue();
                this.InitOwnershipCosts = new OwnershipCosts();

                if (societalDRCount != 0) {
                    this.DiscOpportunityCostSocietal = new double[societalDRCount];
                }
                if (consumerDRCount != 0) {
                    this.DiscOpportunityCostConsumer = new double[consumerDRCount];
                }
            }

            // re-init this veh-eff instance for use with the specified vehicle
            public void ReInitialize(Scenario scen, int year, IVehicle veh, ModelingSettings settings)
            {
                this.IsHistoric = false;

                this.VehRevision = veh.GetRevision(year);
                this.VehBaseInfo = veh.BaselineInfo;

                this.RegClass = this.VehRevision.RegClass;
                this.RCIndex  = RCAltDouble.GetIndex(this.RegClass);
                this.SftClass = this.VehRevision.SafetyClass;
                this.VehClass = this.VehRevision.VehicleClass;
                this.VehStyle = this.VehRevision.Style;

                this.CurbWeight   = this.VehRevision.CurbWeight;
                this.InitCW       = this.VehBaseInfo.CurbWeight;
                this.FuelCapacity = this.VehBaseInfo.FuelCapacity;
                this.Sales        = this.VehRevision.Sales;
                this.MSRP         = this.VehRevision.Msrp;

                this.FS           = Effects.GetModifiedVehicleFS(scen, year, this.VehRevision);
                this.RatedFE      = Effects.CalcRatedVehicleFE(scen, year, this.VehRevision);
                this.OnRoadFE     = Effects.ConvertToOnRoadFE(this.RatedFE, this.VehStyle, settings);
                this.InitFS       = Effects.GetModifiedVehicleFS(scen, year, this.VehBaseInfo);
                this.InitRatedFE  = Effects.CalcRatedVehicleFE(scen, year, this.VehBaseInfo);
                this.InitOnRoadFE = Effects.ConvertToOnRoadFE(this.InitRatedFE, this.VehStyle, settings);
            }

            // re-init this veh-eff instance for use with the specified representative historic vehicle
            public void ReInitialize(int year, VS vehStyle, ModelingSettings settings)
            {
                this.IsHistoric = true;

                this.RegClass = RC.None; // note: reg-class is assigned to the historic veh at time of calc
                this.RCIndex  = -1;
                this.VehClass = vehStyle.ToVehicleClass();
                this.VehStyle = vehStyle;

                var  hfd          = settings.Parameters.HistoricFleetData.GetHistoricFleetData(this.VehStyle, year);
                this.CurbWeight   = hfd.CurbWeight;
                this.InitCW       = hfd.CurbWeight;
                this.FuelCapacity = hfd.FuelCapacity;
                this.Sales        = hfd.InitialProduction;
                this.FS           = hfd.FuelShare;
                this.OnRoadFE     = hfd.OnRoadFE;
                this.RatedFE      = Effects.ConvertToRatedFE(this.OnRoadFE, vehStyle, settings);

                // clear out elements that do not apply to representative historic vehicle
                this.SftClass = SC.None;
                this.Sales = 0;
                this.MSRP = 0;

                // clear out all elements associated with the "initial" vehicle
                // (n/a when evaluating representative historic vehicle)
                this.InitFS = FuelValue.Zero;
                this.InitRatedFE = FuelValue.Zero;
                this.InitOnRoadFE = FuelValue.Zero;
                this.InitFleet = 0;
                this.InitVMT = 0;
                this.InitGallons = 0;
                this.InitUpstream.Clear();
                this.InitTailpipe.Clear();
                this.InitBrakeTireWearPM = 0;
                this.InitOwnershipCosts.Clear();
                this.InitTaxCredits = 0;
                this.InitBatteryTaxCredits = 0;
                this.InitRetailFuelCosts = 0;
                this.InitEmCostsCO2 = 0;
            }

            /// <summary>
            /// Resets all vehicle effects and costs calculated for the preceding fuel-type.
            /// </summary>
            public void ResetFuelSpecificCalcs()
            {
                //--- initial vehicle ---
                // fleet, vmt, and gallons
                this.InitFleet = this.InitVMT = this.InitGallons = 0;
                // emissions effects
                this.InitUpstream.Clear();
                this.InitTailpipe.Clear();
                this.InitBrakeTireWearPM = 0;

                // various costs
                this.InitTaxCredits = 0;
                this.InitBatteryTaxCredits = 0;
                this.InitRetailFuelCosts = 0;
                this.InitEmCostsCO2 = 0;
                this.InitOwnershipCosts.Clear();

                //--- analysis vehicle ---
                // fleet & vmt
                this.Fleet = this.VMT = this.VMTNoRebound = this.VMTReallocated = 0;
                // energy effects
                this.RatedGallons = this.Gallons = this.BTU = this.EnergyUse = 0;
                // safety effects (emission health impacts)
                Array.Clear(this.UpstreamEHI, 0, this.UpstreamEHI.Length);
                Array.Clear(this.TailpipeEHI, 0, this.TailpipeEHI.Length);
                // emissions effects
                this.Upstream.Clear();
                this.Tailpipe.Clear();
                this.BrakeTireWearPM = 0;

                // tech costs
                this.TechCosts = this.RegCosts = this.TaxCredits = this.BatteryTaxCredits = this.MntRprCosts = 0;
                this.OpportunityCostSocietal = 0;
                this.OpportunityCostConsumer = 0;
                if (this.DiscOpportunityCostSocietal != null)
                {
                    Array.Clear(this.DiscOpportunityCostSocietal, 0, this.DiscOpportunityCostSocietal.Length);
                }
                if (this.DiscOpportunityCostConsumer != null)
                {
                    Array.Clear(this.DiscOpportunityCostConsumer, 0, this.DiscOpportunityCostConsumer.Length);
                }
                // ownership costs
                this.OwnershipCosts.Clear();
                // energy costs
                this.RetailFuelCosts = this.FuelTaxCosts = this.ReboundFuelCosts = 0;
                this.DriveSurplus = this.ReallocVMTSurplus = this.RefuelSurplus = 0;
                // costs from market externalities
                this.EconomicCosts = this.CongestionCosts = this.NoiseCosts = 0;
                // emissions costs
                this.EmCosts.Clear();
            }

            //----- global vehicle values (these are inputs and used to inform other calcs) -----
            public bool IsHistoric;

            public IVehicleRevision VehRevision;
            public BaselineVehicle VehBaseInfo;

            public RC RegClass;
            public int RCIndex;
            public SC SftClass;
            public VC VehClass;
            public VS VehStyle;
            public double CurbWeight;
            public double InitCW;
            public double FuelCapacity;
            public double Sales;
            public double MSRP;
            public FuelValue FS, RatedFE, OnRoadFE;
            public FuelValue InitFS, InitRatedFE, InitOnRoadFE;

            //----- values computed per age -----
            // CPM
            public double HistCPM;
            public double CPM;
            public double FleetTotal;
            public double VMTTotal;
            public double VMTNoReboundTotal;
            public double VMTReboundOnlyTotal => this.VMTTotal - this.VMTNoReboundTotal;
            public double VMTReallocTotal;

            //----- values computed per age and fuel type -----
            //--- initial vehicle ---
            // fleet & vmt
            public double InitFleet, InitVMT;
            // energy effects
            public double InitGallons;
            // emissions effects
            public UpstreamEmissionsValue InitUpstream;
            public EmissionsValue InitTailpipe;
            public double InitBrakeTireWearPM;

            // various costs
            public double InitTaxCredits, InitBatteryTaxCredits;
            public OwnershipCosts InitOwnershipCosts;
            public double InitRetailFuelCosts;
            public double InitEmCostsCO2;

            //--- analysis vehicle ---
            // fleet & vmt
            public double Fleet;
            public double VMT;
            public double VMTNoRebound;
            public double VMTReboundOnly => this.VMT - this.VMTNoRebound;
            public double VMTReallocated;
            // energy effects
            public double RatedGallons, Gallons, BTU, EnergyUse;
            // safety effects (emission health impacts)
            public double[] UpstreamEHI;
            public double[] TailpipeEHI;
            // emissions effects
            public UpstreamEmissionsValue Upstream;
            public EmissionsValue Tailpipe;
            public double BrakeTireWearPM;

            // tech costs & value losses
            public double TechCosts, RegCosts, TaxCredits, BatteryTaxCredits, MntRprCosts;
            public double OpportunityCostSocietal;
            public double[] DiscOpportunityCostSocietal;
            public double OpportunityCostConsumer;
            public double[] DiscOpportunityCostConsumer;
            // ownership costs
            public OwnershipCosts OwnershipCosts;
            // energy costs
            public double RetailFuelCosts, FuelTaxCosts, ReboundFuelCosts;
            public double DriveSurplus, ReallocVMTSurplus, RefuelSurplus;
            // costs from market externalities
            public double EconomicCosts, CongestionCosts, NoiseCosts;
            // emissions costs
            public EmissionsCosts EmCosts;

            // vehicle paybacks (undiscounted only)
            public double PaybackSum;
            public double PaybackTCOSum;
            public double Payback = PaybackInitValue;
            public double PaybackTCO = PaybackInitValue;
        }

        /// <summary>
        /// Provides the emission damage costs arising from damage caused by various vehicle emissions.
        /// </summary>
        class EmissionsCosts
        {
            public void Clear()
            {
                // criteria pollutants
                this.CO = 0;
                this.VOC = 0;

                // criteria pollutants pre-discounted at 3% or 7%
                this.NOx.Clear();
                this.SO2.Clear();
                this.PM.Clear();

                // ghg pollutants
                this.CO2.Clear();
                this.CH4.Clear();
                this.N2O.Clear();
            }

            // criteria pollutants
            public double CO;
            public double VOC;

            // criteria pollutants pre-discounted at 3% or 7%
            public ValueByDiscRate NOx = new ValueByDiscRate();
            public ValueByDiscRate SO2 = new ValueByDiscRate();
            public ValueByDiscRate PM = new ValueByDiscRate();

            // ghg pollutants at various estimates (low, average, high, very high)
            public ValueByEstimate CO2 = new ValueByEstimate();
            public ValueByEstimate CH4 = new ValueByEstimate();
            public ValueByEstimate N2O = new ValueByEstimate();
        }

        //
        // Variables to aid in computing NewSalesSurplus
        //
        class NewSalesSurplusValues
        {
            public double Sales;
            public double RegCost;
            public FuelValue OnRoadFE;
            public FuelValue FuelShare;
        }

        #endregion

        #region <<< Ctors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="EffectsModel"/> class.
        /// </summary>
        /// <param name="complianceModel">The compliance model associated with this <see cref="EffectsModel"/>
        ///     instance.</param>
        public EffectsModel(ICompliance complianceModel)
        {
            this.ComplianceModel = complianceModel;
            this._baseVehEff = new Dictionary<int, BaselineVehicleEffects[]>();
            this._baseNewSalesSurplusValues = new Dictionary<int, NewSalesSurplusValues>();
            this._baseCommercialOppCost = new Dictionary<int, RCAltFuelValue[][]>();
        }

        #endregion

        #region <<< Methods >>>

        #region < Effects calculations for historic years >

        /// <summary>
        /// Calculates effects for the historic fleet in the specified scenario and model year.
        /// </summary>
        /// <param name="scen">The current scenario being analyzed.</param>
        /// <param name="year">The analysis year for which to calculate historic effects.</param>
        /// <param name="altRegClassMix">The variety of regulatory classes represented within the industry.</param>
        /// <param name="vmtModel">The VMT model to use for estimating vehicle miles traveled at each age.</param>
        /// <param name="scrappageModel">The scrappage model to use for obtaining the scrapped historic fleet.</param>
        /// <returns>The effects data calculated for the historic fleet.</returns>
        public EffectsData.ModelingEffects CalculateHistoricEffects(
            Scenario scen, int year, RCAltValue<bool> altRegClassMix, VMTModel vmtModel, ScrappageModel scrappageModel)
        {
            bool hasLD = altRegClassMix[RC.AllPassengerCar] || altRegClassMix[RC.LightTruck];
            bool has2b3 = altRegClassMix[RC.HDPUV];

            this.InitBaseVehEff(scen, year, VehicleStyles.AltList.Length);

            // get the annual fleet data from the scrappage model
            var fleetData = scrappageModel.GetScrappedFleet(year);

            // calculate and aggregate historic effects for each represented reg-class
            var ed = new EffectsData.ModelingEffects(year, this.Parameters.EconomicValues.DiscountRate, null);
            for (int i = 0; i < VehicleStyles.AltList.Length; i++)
            {
                VS vehStyle = VehicleStyles.AltList[i];
                bool isLD = !vehStyle.Is2b3();
                bool is2b3 = vehStyle.Is2b3();

                if ((isLD && hasLD) || (is2b3 && has2b3)) {
                    this.CalculateHistoricVehicleEffectsAndCosts(
                        scen, year, i, vehStyle, vmtModel, fleetData[vehStyle], ed);
                }
            }

            // finalize calculating certain effects and costs once all data was aggregated
            this.CalculateDiscountedSocialCosts(year, ed);
            this.CalcCommercialOppCost(scen, year, ed);
            ed.AggregateAnnualEffects();

            return ed;
        }

        void CalculateHistoricVehicleEffectsAndCosts(Scenario scen, int year, int vehIndex, VS vehStyle, VMTModel vmtModel,
            double[] fleetData, EffectsData.ModelingEffects ed)
        {
            // prepare representative historic "vehicle" for processing effects
            var vehEff = new VehicleEffects();
            vehEff.ReInitialize(year, vehStyle, this.Settings);

            if (scen.IsBaseline) {
                this._baseVehEff[year][vehIndex] = new BaselineVehicleEffects(vehEff);
            }
            var baseVehEff = this.GetBaseVehEff(year, vehIndex);

            // calculate annual historic vehicle effects for each vehicle age and fuel type
            for (int age = this.MinYear - year; age < Globals.CY; age++)
            {
                var annualEff = ed.AnnualEffects[age];

                // reset "totals" accumulated across all fuel types
                vehEff.FleetTotal = 0;
                vehEff.VMTTotal = 0;
                vehEff.VMTNoReboundTotal = 0;
                vehEff.VMTReallocTotal = 0;

                foreach (FT fuelType in FuelTypes.List)
                {
                    if (vehEff.FS[fuelType] == 0) { continue; }

                    // for the historic fleet, some veh-styles may simultaneously represent a
                    // portion of the PC and LT reg-classes
                    // we use the "PCShare" variable from the historic fleet to determine what
                    // share of calculated vehicle effects should be apportioned to each class
                    // for HDPUV vehicles, however, there is a one-to-one mapping between the
                    // veh-style and the reg-class
                    if (vehStyle.Is2b3())
                    {
                        this.CalcHistoricVehicleEffectsAndCosts(
                            year, age, RC.HDPUV, fuelType, vehEff, vmtModel, fleetData, 1, annualEff);
                    }
                    else
                    {
                        var hfd = this.Parameters.HistoricFleetData.GetHistoricFleetData(vehStyle, year);

                        this.CalcHistoricVehicleEffectsAndCosts(
                            year, age, RC.AllPassengerCar, fuelType, vehEff, vmtModel, fleetData, hfd.PCShare, annualEff);
                        this.CalcHistoricVehicleEffectsAndCosts(
                            year, age, RC.LightTruck, fuelType, vehEff, vmtModel, fleetData, 1 - hfd.PCShare, annualEff);
                    }

                    // at the end of each age and fuel type calc, save a snapshot of vehicle-level
                    // effects from the baseline scenario required for incremental fatality-related
                    // calculations in alternative scenarios
                    if (scen.IsBaseline)
                    {
                        this._baseVehEff[year][vehIndex].SaveAgeAndFuelTypeInfo(age, fuelType, vehEff);
                    }
                }

                // fatalities are calculated as incremental over the baseline vehicle and cannot
                // be computed by fuel type -->
                //  - calculate total fatalities, then apportion it by fuel share
                //  - calculate fatality costs,
                //  - aggregate fatalities and fatality costs
                this.CalculateVehicleFatalities(scen, year, age, vehEff, baseVehEff, annualEff);
            } // next age
        }
        void CalcHistoricVehicleEffectsAndCosts(int year, int age, RC regClass, FT fuelType, VehicleEffects vehEff,
            VMTModel vmtModel, double[] fleetData, double fleetShare, ModelingEffectsItem annualEff)
        {
            if (fleetShare > 0)
            {
                vehEff.RegClass = regClass;
                vehEff.RCIndex  = RCAltDouble.GetIndex(regClass);
                vehEff.ResetFuelSpecificCalcs();

                this.CalculateVehicleEffects        (year, age, fuelType, vehEff, vmtModel, null, fleetData, fleetShare);
                this.CalculateVehicleCosts          (year, age, fuelType, vehEff, null);
                this.AggregateVehicleEffectsAndCosts(year, age, fuelType, vehEff, annualEff);
            }
        }

        #endregion

        #region < Effects calculations for analysis years >

        /// <summary>
        /// Calculates effects for the specified vehicles in the specified scenario and model year.
        /// </summary>
        /// <param name="scen">The current scenario being analyzed.</param>
        /// <param name="year">The current model year being analyzed.</param>
        /// <param name="vehs">A list of vehicles, for the current scenario and model year being analyzed, for which to
        ///     calculate and aggregate the effects.</param>
        /// <param name="vmtModel">The VMT model to use for estimating vehicle miles traveled at each age.</param>
        /// <param name="scrappageModel">The scrappage model to use for obtaining the vehicle survival rates.</param>
        /// <returns>The effects data aggregated from individual vehicles.</returns>
        public ModelingEffects CalculateEffects(
            Scenario scen, int year, IReadOnlyList<IVehicle> vehs, VMTModel vmtModel, ScrappageModel scrappageModel)
        {
            this.InitBaseVehEff(scen, year, vehs.Count);

            // get the survival rates schedule from the scrappage model
            var survRates = scrappageModel.GetSurvivalRates(year);

            // calculate effects and costs for each individual vehicle
            var econVals = this.Parameters.EconomicValues;
            var effData = new ModelingEffects(year, econVals.DiscountRate, econVals.ConsumerDiscRate);

            for (int i = 0; i < vehs.Count; i++)
            {
                this.CalculateVehicleEffectsAndCosts(scen, year, i, vehs[i], vmtModel, survRates, effData);
            }

            // calculate certain fleet-wide aggregate metrics
            this.CalculateGeneralEffects (scen, year, vehs, effData.AggregateEffects);
            this.CalculateNewSalesSurplus(scen, year, vehs, effData);

            // finalize calculating certain effects and costs once all data was aggregated
            this.CalculateDiscountedSocialCosts  (year, effData);
            this.CalculateDiscountedConsumerCosts(year, effData);
            this.CalcCommercialOppCost(scen, year, effData);
            effData.AggregateAnnualEffects();

            // consumer paybacks have to be calculated last
            this.CalculateConsumerPaybacks(effData);

            return effData;
        }

        void CalculateVehicleEffectsAndCosts(Scenario scen, int year,
            int vehIndex, IVehicle veh, VMTModel vmtModel, VSValue<double[]> survRates, ModelingEffects eff)
        {
            // prepare vehicle for processing effects
            var econValues = this.Parameters.EconomicValues;
            var vehEff = new VehicleEffects(econValues.DiscountRate.Length, econValues.ConsumerDiscRate.Length);
            vehEff.ReInitialize(scen, year, veh, this.Settings);

            if (scen.IsBaseline) {
                this._baseVehEff[year][vehIndex] = new BaselineVehicleEffects(vehEff);
            }

            var baseVehEff = this.GetBaseVehEff(year, vehIndex);
            if (vehEff.Sales == 0 && baseVehEff.Sales == 0) {
                return;
            }

            this.ResetOwnershipCostsAndPaybacksOnVehicle(vehEff);

            // calculate annual vehicle effects for each calendar year
            double[] survRate = survRates[vehEff.VehStyle];

            for (int age = 0; age < Globals.CY; age++)
            {
                if (survRate[age] == 0) {
                    break;
                }

                // calculate cost per mile for the current vehicle age
                this.CalculateCPMs(year, age, vehEff);

                // reset "totals" accumulated across all fuel types
                vehEff.FleetTotal = 0;
                vehEff.VMTTotal = 0;
                vehEff.VMTNoReboundTotal = 0;
                vehEff.VMTReallocTotal = 0;

                // calculate effects for each fuel type vehicle uses
                foreach (FT fuelType in FuelTypes.List)
                {
                    if (vehEff.InitFS[fuelType] == 0 && vehEff.FS[fuelType] == 0) {
                        continue;
                    }

                    vehEff.ResetFuelSpecificCalcs();

                    // "initial" vehicle
                    if (vehEff.InitFS[fuelType] != 0)
                    {
                        this.CalculateInitVehEffects(year, age, fuelType, vehEff, vmtModel, survRate);
                        this.CalculateInitVehCosts  (year, age, fuelType, vehEff);
                        this.AggregateInitVehEffectsAndCosts(   fuelType, vehEff, eff.AnnualEffects[age]);
                    }

                    // analysis vehicle
                    if (vehEff.FS[fuelType] != 0)
                    {
                        this.CalculateVehicleEffects        (year, age, fuelType, vehEff, vmtModel, survRate, null, 1);
                        this.CalculateVehicleCosts          (year, age, fuelType, vehEff, veh);
                        this.AggregateVehicleEffectsAndCosts(year, age, fuelType, vehEff, eff.AnnualEffects[age]);
                    }

                    this.AggregateOwnershipCostsToTheVehicle(fuelType, vehEff);
                    this.AggregateVehiclePaybackLevels(vehEff);

                    // at the end of each age and fuel type calc, save a snapshot of vehicle-level
                    // effects from the baseline scenario required for incremental fatality-related
                    // calculations in alternative scenarios
                    if (scen.IsBaseline) {
                        baseVehEff.SaveAgeAndFuelTypeInfo(age, fuelType, vehEff);
                    }
                }

                // reallocated VMT surplus is calculated as incremental over the baseline vehicle
                // and cannot be computed by fuel type -->
                //  - calculate total reallocated VMT surplus,
                //  - apportion it by fuel share,
                //  - then aggregate
                this.CalculateVehicleReallocatedVmtSurplus(scen, year, age, vehEff, baseVehEff, eff.AnnualEffects[age]);

                // fatalities are calculated as incremental over the baseline vehicle and cannot
                // be computed by fuel type -->
                //  - calculate total fatalities, then apportion it by fuel share
                //  - calculate fatality costs,
                //  - aggregate fatalities and fatality costs
                this.CalculateVehicleFatalities(scen, year, age, vehEff, baseVehEff, eff.AnnualEffects[age]);

                // once all fuel types used by the vehicle have been evaluated, determine if, at
                // the current age, the accumulated fuel savings outweigh the accumulated costs
                this.UpdateVehiclePaybackAge(age, vehEff);
            } // next age
        }

        void ResetOwnershipCostsAndPaybacksOnVehicle(VehicleEffects vehEff)
        {
            // ownership costs and undiscounted payback levels calculated by the effects model
            // should be saved to the analysis vehicle -- reset them here to get a clean slate
            var vehRevision = vehEff.VehRevision;
            vehRevision.InitOwnershipCosts.Clear();
            vehRevision.OwnershipCosts.Clear();
            vehRevision.Payback = PaybackInitValue;
            vehRevision.PaybackTCO = PaybackInitValue;
        }
        void AggregateOwnershipCostsToTheVehicle(FT fuelType, VehicleEffects vehEff)
        {
            // ownership costs calculated by the effects model should be saved
            // to the vehicle's revision for the specific model year
            var vehRevision = vehEff.VehRevision;

            if (vehEff.InitFS[fuelType] != 0)
            {
                var initialOpCosts = vehRevision.InitOwnershipCosts;
                initialOpCosts.TaxesAndFees   += vehEff.InitOwnershipCosts.TaxesAndFees   / vehEff.Sales * 1000; // convert back from $k
                initialOpCosts.FinancingCosts += vehEff.InitOwnershipCosts.FinancingCosts / vehEff.Sales * 1000;
                initialOpCosts.InsuranceCosts += vehEff.InitOwnershipCosts.InsuranceCosts / vehEff.Sales * 1000;
            }
            if (vehEff.FS[fuelType] != 0)
            {
                var opCosts = vehRevision.OwnershipCosts;
                opCosts.TaxesAndFees   += vehEff.OwnershipCosts.TaxesAndFees   / vehEff.Sales * 1000; // convert back from $k
                opCosts.FinancingCosts += vehEff.OwnershipCosts.FinancingCosts / vehEff.Sales * 1000;
                opCosts.InsuranceCosts += vehEff.OwnershipCosts.InsuranceCosts / vehEff.Sales * 1000;
            }
        }

        void AggregateVehiclePaybackLevels(VehicleEffects vehEff)
        {
            // aggregate undiscounted vehicle payback levels for the fuel type
            double pbSum = (vehEff.RegCosts
                          + vehEff.RetailFuelCosts
                          - vehEff.InitRetailFuelCosts
                          - (vehEff.TaxCredits - vehEff.InitTaxCredits)
                          - (vehEff.BatteryTaxCredits - vehEff.InitBatteryTaxCredits));

            vehEff.PaybackSum += pbSum;
            vehEff.PaybackTCOSum += pbSum +
                (vehEff.MntRprCosts + vehEff.OwnershipCosts.Total - vehEff.InitOwnershipCosts.Total);
        }
        void UpdateVehiclePaybackAge(int age, VehicleEffects vehEff)
        {
            // update vehicle payback to given age if payback has not been set yet
            var vehRevision = vehEff.VehRevision;

            if (vehEff.Payback == PaybackInitValue && vehEff.PaybackSum <= 0) {
                vehEff.Payback = age;
                vehRevision.Payback = age;
            }

            if (vehEff.PaybackTCO == PaybackInitValue && vehEff.PaybackTCOSum <= 0) {
                vehEff.PaybackTCO = age;
                vehRevision.PaybackTCO = age;
            }
        }

        void CalculateGeneralEffects(
            Scenario scen, int year, IReadOnlyList<IVehicle> vehs, ModelingEffectsItem aggregateEff)
        {
            // calculate general effects for each fuel type
            foreach (FT fuelType in FuelTypes.List) {
                int ft = fuelType.GetListIndex();

                var sales = new RCAltDouble();
                var cw = new RCAltDouble();
                var fp = new RCAltDouble();
                var wf = new RCAltDouble();

                // aggregate
                foreach (var veh in vehs) {
                    var vehRevision = veh.GetRevision(year);
                    double vehFS = Effects.GetModifiedVehicleFS(scen, year, vehRevision)[fuelType];

                    if (vehFS != 0) {
                        // vehicle uses the current fuel type
                        int vehRC = RCAltDouble.GetIndex(vehRevision.RegClass);
                        double vehSalesFS = vehRevision.Sales * vehFS;
                        sales.Items[vehRC] += vehSalesFS;
                        cw.Items[vehRC] += vehSalesFS * vehRevision.CurbWeight;
                        fp.Items[vehRC] += vehSalesFS * vehRevision.Footprint;
                        wf.Items[vehRC] += vehSalesFS * vehRevision.GetWorkFactor();
                    }
                }

                // finalize
                for (int rc = 0; rc < sales.Items.Length; rc++) {
                    var generalEffects = aggregateEff.GeneralEffects[rc, ft];
                    double salesRC = sales.Items[rc];

                    if (salesRC == 0) {
                        generalEffects.Sales = 0;
                        generalEffects.CurbWeight = 0;
                        generalEffects.Footprint = 0;
                        generalEffects.WorkFactor = 0;
                    }
                    else {
                        generalEffects.Sales = salesRC;
                        generalEffects.CurbWeight = cw.Items[rc] / salesRC;
                        generalEffects.Footprint = fp.Items[rc] / salesRC;
                        generalEffects.WorkFactor = wf.Items[rc] / salesRC;
                    }
                }
            }
        }

        void CalculateNewSalesSurplus(Scenario scen, int year, IReadOnlyList<IVehicle> vehs, ModelingEffects effData)
        {
            double      sales     = 0;
            double      regCost   = 0;
            FuelValue   onRoadFE  = FuelValue.Zero;
            FuelValue   fuelShare = FuelValue.Zero;
            RCAltDouble salesByRC = RCAltDouble.Zero;

            foreach (var veh in vehs)
            {
                var vehRevision = veh.GetRevision(year);
                double vehSales = vehRevision.Sales;
                if (vehSales == 0) {
                    continue;
                }

                FuelValue vehFS = Effects.GetModifiedVehicleFS(scen, year, vehRevision);
                FuelValue vehSalesFS  = vehSales * vehFS;
                FuelValue vehOnRoadFE = Effects.ConvertToOnRoadFE(vehRevision.FuelEconomy, vehRevision.Style, this.Settings);

                sales     += vehSales;
                regCost   += vehSales * vehRevision.ComplianceCosts.RegCost;
                onRoadFE  += FuelValue.SafeDivide(vehSalesFS, vehOnRoadFE);
                fuelShare += vehSalesFS;
                salesByRC[vehRevision.RegClass] += vehSales;
            }

            regCost   /= sales;
            onRoadFE   = FuelValue.SafeDivide(fuelShare, onRoadFE);
            fuelShare /= sales;

            if (scen.IsBaseline)
            {
                this._baseNewSalesSurplusValues.Add(year, new NewSalesSurplusValues()
                {
                    Sales     = sales,
                    RegCost   = regCost,
                    OnRoadFE  = onRoadFE,
                    FuelShare = fuelShare
                });
            }
            else
            {
                var    baseValues   = this._baseNewSalesSurplusValues[year];
                double deltaSales   = baseValues.Sales - sales;
                double deltaRegCost = regCost - baseValues.RegCost;
                double fuelSavings  = Effects.CalcAssumedFuelSavings(year, baseValues.OnRoadFE, baseValues.FuelShare,
                                        onRoadFE, fuelShare, this.Settings);
                double totalNewSalesSurplus = (deltaRegCost - fuelSavings) * deltaSales / 2 / 1000; // convert to $k

                // new sales surplus occurs at age 0 only --
                // save new sales surplus for undiscounted social and consumer costs (indexed at 0)
                var socialCosts   = effData.AnnualEffects[0].SocialCosts;
                var consumerCosts = effData.AnnualEffects[0].ConsumerCosts;

                foreach (RC regClass in RegClasses.AltList)
                {
                    double newSalesSurplus = totalNewSalesSurplus * (salesByRC[regClass] / sales);

                    foreach (FT fuelType in FuelTypes.List) {
                        socialCosts[regClass, fuelType].NewSalesSurplus = newSalesSurplus * fuelShare[fuelType];
                    }
                    consumerCosts[regClass].NewSalesSurplus = newSalesSurplus;
                }
            }
        }

        void CalculateConsumerPaybacks(ModelingEffects effData)
        {
            var aggregateEff = effData.AggregateEffects;
            var annualEff = effData.AnnualEffects;

            this.CalculateConsumerPaybacks(aggregateEff.ConsumerCosts, age => annualEff[age].ConsumerCosts);

            for (int i = 0; i < aggregateEff.DiscConsumerCosts.Length; i++) {
                this.CalculateConsumerPaybacks(
                    aggregateEff.DiscConsumerCosts[i], age => annualEff[age].DiscConsumerCosts[i]);
            }
        }

        void CalculateConsumerPaybacks(ConsumerCostsCollection consumerCosts, Func<int, ConsumerCostsCollection> getAnnualCosts)
        {
            foreach (RC regClass in RegClasses.AltList) {
                var consumerCostsRC = consumerCosts[regClass];
                double payback = PaybackInitValue, paybackTCO = PaybackInitValue;
                double pbSum = 0, tcoSum = 0;

                for (int age = 0; age < Globals.CY; age++) {
                    ConsumerCosts cc = getAnnualCosts(age)[regClass];

                    double prvDR = consumerCosts.DiscountRate;
                    prvDR = (prvDR == 0) ? 1 : Math.Pow(1 + prvDR, -age);

                    double currPB = prvDR * (cc.RegCosts
                                           + cc.RetailFuelCosts
                                           - cc.InitialRetailFuelCosts
                                           - (cc.TaxCredits - cc.InitTaxCredits)
                                           - (cc.BatteryTaxCredits - cc.InitBatteryTaxCredits));

                    pbSum  += currPB;
                    tcoSum += currPB + prvDR * (cc.MaintRepairCosts
                                              + cc.OwnershipCosts.Total
                                              - cc.InitOwnershipCosts.Total);

                    if (payback == PaybackInitValue && pbSum <= 0) {
                        payback = age;
                    }
                    if (paybackTCO == PaybackInitValue && tcoSum <= 0) {
                        paybackTCO = age;
                    }
                }

                consumerCostsRC.Payback = payback;
                consumerCostsRC.PaybackTCO = paybackTCO;
            }
        }

        #endregion

        #region < Common "per-vehicle" calculations, aggregations, and discounting of costs >

        void InitBaseVehEff(Scenario scen, int year, int vehCount)
        {
            if (scen.IsBaseline)
            {
                this._baseVehEff.Add(year, new BaselineVehicleEffects[vehCount]);
            }
        }
        BaselineVehicleEffects GetBaseVehEff(int year, int vehIndex)
        {
            return this._baseVehEff[year][vehIndex];
        }

        void CalculateCPMs(int year, int age, VehicleEffects vehEff)
        {
            int vmtRefYr = this.Parameters.EconomicValues.Parameters[vehEff.VehStyle].VMTBaseYear;
            var hstFleet = this.Parameters.HistoricFleetData.GetHistoricFleetData(vehEff.VehStyle, vmtRefYr - age);

            vehEff.HistCPM = Effects.CalcCPM(year + age, hstFleet.OnRoadFE, hstFleet.FuelShare, this.Settings);
            vehEff.CPM = Effects.CalcCPM(year + age, vehEff.OnRoadFE, vehEff.FS, this.Settings);
        }

        /// <summary>
        /// Calculates vmt, fuel use (in gallons), and CO-2 emissions applicable to an "initial" vehicle.
        /// </summary>
        void CalculateInitVehEffects(int year, int age, FT fuelType, VehicleEffects vehEff, VMTModel vmtModel,
            double[] survRate)
        {
            //--- fleet & vehicle miles traveled ---
            vehEff.InitFleet = vehEff.Sales * survRate[age] * vehEff.InitFS[fuelType];
            vehEff.InitVMT   = vehEff.InitFleet / 1000 *
                               vmtModel.GetVMT(year, age, vehEff.VehStyle, vehEff.InitOnRoadFE, vehEff.InitFS);

            //--- energy effects ---
            this.CalcVehicleEnergyEffects(fuelType, FuelValue.Zero, vehEff.InitOnRoadFE, vehEff.InitVMT,
                out _, out vehEff.InitGallons, out double initBTU, out double initEnergyUse);

            //--- emissions effects ---
            this.CalcVehicleEmissionEffects(year, age, fuelType,
                vehEff.VehClass, vehEff.InitVMT, initBTU, initEnergyUse, vehEff.InitUpstream, vehEff.InitTailpipe,
                out vehEff.InitBrakeTireWearPM);
        }

        /// <summary>
        /// Calculates all effects for:
        ///     (1) a vehicle model evaluated and updated during analysis
        ///             -or-
        ///     (2) a representative historic vehicle.
        /// </summary>
        void CalculateVehicleEffects(int year, int age, FT fuelType, VehicleEffects vehEff, VMTModel vmtModel,
            double[] survRate, double[] fleetData, double fleetShare)
        {
            //--- fleet & vehicle miles traveled ---
            vehEff.Fleet          = (survRate == null ? fleetData[age] * fleetShare : vehEff.Sales * survRate[age]) * vehEff.FS[fuelType];
            vehEff.VMTNoRebound   = vehEff.Fleet / 1000 * vmtModel.GetNonReboundVMT(year, age, vehEff.VehStyle, vehEff.FS);
            vehEff.VMT            = vehEff.Fleet / 1000 * vmtModel.GetVMT(year, age, vehEff.VehStyle, vehEff.OnRoadFE, vehEff.FS);
            vehEff.VMTReallocated = vehEff.Fleet / 1000 * vmtModel.GetReallocatedVMT(year, age, vehEff.VehStyle);

            // accumulate fleet/vmt totals from all fuel types for this vehicle
            vehEff.FleetTotal        += vehEff.Fleet;
            vehEff.VMTTotal          += vehEff.VMT;
            vehEff.VMTNoReboundTotal += vehEff.VMTNoRebound;
            vehEff.VMTReallocTotal   += vehEff.VMTReallocated;

            //--- energy effects ---
            this.CalcVehicleEnergyEffects(fuelType, vehEff.RatedFE, vehEff.OnRoadFE, vehEff.VMT,
                out vehEff.RatedGallons, out vehEff.Gallons, out vehEff.BTU, out vehEff.EnergyUse);

            //--- emissions effects ---
            this.CalcVehicleEmissionEffects(year, age, fuelType,
                vehEff.VehClass, vehEff.VMT, vehEff.BTU, vehEff.EnergyUse, vehEff.Upstream, vehEff.Tailpipe,
                out vehEff.BrakeTireWearPM);

            //--- safety effects (emission health impacts) ---
            this.CalcVehicleEmissionHealthImpacts(year, fuelType, age, vehEff);
        }

        // calc vehicle effects -- helper methods
        void CalcVehicleEnergyEffects(FT fuelType, FuelValue ratedFE, FuelValue onRoadFE, double vmt,
            out double ratedGallons, out double gallons, out double btu, out double energyUse)
        {
            // compute fuel consumption in gallons or gasoline equivalent gallons (GEG) for rated and on-road FE
            ratedGallons = vmt / ratedFE [fuelType];
            gallons      = vmt / onRoadFE[fuelType];

            // BTU and energy use for a specific fuel type
            //  * for non-liquid fuels (electricity, hydrogen, cng), energy use must be converted from GEG to scf/kwh
            //  * for liquid fuels (gasoline, diesel, etc.), no conversion for energy use is required (just use gallons)
            var fuelProperties = this.Parameters.FuelProperties;
            if (fuelType.IsGEGFuel())
            {
                btu       = gallons * fuelProperties.EnergyDensity.Gasoline;
                energyUse = btu     / fuelProperties.EnergyDensity[fuelType];
            }
            else
            {
                btu       = gallons * fuelProperties.EnergyDensity[fuelType];
                energyUse = gallons;
            }
        }
        void CalcVehicleEmissionEffects(int year, int age, FT fuelType, VC vehClass, double vmt, double btu,
            double energyUse, UpstreamEmissionsValue upstream, EmissionsValue tailpipe, out double breakTireWearPM)
        {
            var fuelProperties    = this.Parameters.FuelProperties;
            var upstreamEmissions = this.Parameters.UpstreamEmissions[fuelType, year + age];
            var tailpipeGasoline  = this.Parameters.TailpipeGasoline [vehClass, year, age];
            var tailpipeDiesel    = this.Parameters.TailpipeDiesel   [vehClass, year, age];
            var fuelAssumptions   = this.Parameters.FuelImportAssumptions[fuelType, year + age];

            double shareOfFSLeadingReducedDomRef = fuelAssumptions.FuelSavingsLeadingToReducedRefining;
            double shareOfReducedRefFromDomCrude = fuelAssumptions.ReducedRefiningFromDomesticCrude;

            foreach (var pollutant in Pollutants.List)
            {
                var ueParamsByCategory = upstreamEmissions[pollutant];
                var upstreamByCategory = upstream[pollutant];

                bool isCO2 = (pollutant == Pollutant.CO2);
                bool isGHG = (isCO2 || pollutant == Pollutant.CH4 || pollutant == Pollutant.N2O);
                bool isSO2 = (pollutant == Pollutant.SO2);
                bool isGas = (fuelType == FT.Gasoline || fuelType == FT.Ethanol85);
                bool isDsl = (fuelType == FT.Diesel);

                // note: calculation converts units to metric tons
                double btuMetricTons = btu / 1e9;

                if (fuelType.IsGEGFuel())
                {
                    upstreamByCategory.Total = ueParamsByCategory.Total * btuMetricTons;
                }
                else
                {
                    upstreamByCategory.FuelTSD        = ueParamsByCategory.FuelTSD        * btuMetricTons;
                    upstreamByCategory.Refining       = ueParamsByCategory.Refining       * btuMetricTons;
                    upstreamByCategory.Extraction     = ueParamsByCategory.Extraction     * btuMetricTons;
                    upstreamByCategory.Transportation = ueParamsByCategory.Transportation * btuMetricTons;

                    if (!isGHG)
                    {
                        upstreamByCategory.Refining       *= shareOfFSLeadingReducedDomRef;
                        upstreamByCategory.Extraction     *= shareOfFSLeadingReducedDomRef * shareOfReducedRefFromDomCrude;
                        upstreamByCategory.Transportation *= shareOfFSLeadingReducedDomRef * shareOfReducedRefFromDomCrude;
                    }

                    upstreamByCategory.Total = upstreamByCategory.FuelTSD
                                             + upstreamByCategory.Refining
                                             + upstreamByCategory.Extraction
                                             + upstreamByCategory.Transportation;
                }

                tailpipe[pollutant] =
                    (isCO2) ? fuelProperties.CO2Emissions[fuelType] * energyUse / 1e3 :
                    (isSO2) ? fuelProperties.SO2Emissions[fuelType] * btuMetricTons :
                    (isGas) ? tailpipeGasoline[pollutant] * vmt / 1e3 :
                    (isDsl) ? tailpipeDiesel  [pollutant] * vmt / 1e3 : 0;
            }

            var btwEmissions = this.Parameters.BrakeTireWearEmissions[year, vehClass, fuelType];
            breakTireWearPM = btwEmissions.Total * vmt / 1e3;
        }
        void CalcVehicleEmissionHealthImpacts(int year, FT fuelType, int age, VehicleEffects vehEff)
        {
            int calendarYear = year + age;
            foreach (EmissionHealthImpact ehi in HealthImpacts.List)
            {
                this.CalcVehicleEHI(calendarYear, fuelType, vehEff, ehi);
            }
        }
        void CalcVehicleEHI(int calendarYear, FT fuelType, VehicleEffects vehEff, EmissionHealthImpact healthImpact)
        {
            var healthImpacts = this.Parameters.EmissionHealthImpacts[healthImpact, calendarYear];

            // health impacts for upstream emissions
            double healthUpstreamNOX;
            double healthUpstreamSO2;
            double healthUpstreamPM;

            if (fuelType.IsGEGFuel())
            {
                healthUpstreamNOX = vehEff.Upstream.NOx.Total * healthImpacts.UpstreamElectricity.NOX;
                healthUpstreamSO2 = vehEff.Upstream.SO2.Total * healthImpacts.UpstreamElectricity.SO2;
                healthUpstreamPM = vehEff.Upstream.PM.Total * healthImpacts.UpstreamElectricity.PM;
            }
            else
            {
                healthUpstreamNOX =
                    vehEff.Upstream.NOx.Refining       * healthImpacts.UpstreamRefineries.NOX +
                    vehEff.Upstream.NOx.Extraction     * healthImpacts.UpstreamExtraction.NOX +
                    vehEff.Upstream.NOx.Transportation * healthImpacts.UpstreamTransportation.NOX +
                    vehEff.Upstream.NOx.FuelTSD        * healthImpacts.UpstreamFuelTSD.NOX;
                healthUpstreamSO2 =
                    vehEff.Upstream.SO2.Refining       * healthImpacts.UpstreamRefineries.SO2 +
                    vehEff.Upstream.SO2.Extraction     * healthImpacts.UpstreamExtraction.SO2 +
                    vehEff.Upstream.SO2.Transportation * healthImpacts.UpstreamTransportation.SO2 +
                    vehEff.Upstream.SO2.FuelTSD        * healthImpacts.UpstreamFuelTSD.SO2;
                healthUpstreamPM =
                    vehEff.Upstream.PM.Refining        * healthImpacts.UpstreamRefineries.PM +
                    vehEff.Upstream.PM.Extraction      * healthImpacts.UpstreamExtraction.PM +
                    vehEff.Upstream.PM.Transportation  * healthImpacts.UpstreamTransportation.PM +
                    vehEff.Upstream.PM.FuelTSD         * healthImpacts.UpstreamFuelTSD.PM;
            }

            vehEff.UpstreamEHI[(int)healthImpact] = MetricToShortTon * (healthUpstreamNOX + healthUpstreamSO2 + healthUpstreamPM);

            // health impacts for downstream emissions
            CriteriaEmissionsValue downstreamSources = null;
            if (fuelType == FT.Diesel)
            {
                downstreamSources = vehEff.VehClass.IsLDT2b3()
                                  ? healthImpacts.DownstreamDsl2b3
                                  : healthImpacts.DownstreamDslLDx;
            }
            else if (fuelType == FT.Gasoline || fuelType == FT.Ethanol85)
            {
                downstreamSources = vehEff.VehClass.IsLDT2b3() ? healthImpacts.DownstreamGas2b3
                                  : vehEff.VehClass.IsLDT12a() ? healthImpacts.DownstreamGasLDT
                                  :                              healthImpacts.DownstreamGasLDV;
            }

            if (downstreamSources != null) {
                var vehTailpipe = vehEff.Tailpipe;
                vehEff.TailpipeEHI[(int)healthImpact] = MetricToShortTon * (
                    vehTailpipe.NOx * downstreamSources.NOX +
                    vehTailpipe.SO2 * downstreamSources.SO2 +
                    (vehTailpipe.PM + vehEff.BrakeTireWearPM) * downstreamSources.PM);
            }
        }

        /// <summary>
        /// Calculates a subset of undiscounted societal and consumer costs applicable to an "initial" vehicle.
        /// </summary>
        void CalculateInitVehCosts(int year, int age, FT fuelType, VehicleEffects vehEff)
        {
            int calendarYear = year + age;

            if (age == 0 && !vehEff.IsHistoric) {
                double salesFS = vehEff.Sales * vehEff.InitFS[fuelType] / 1000;

                vehEff.InitTaxCredits = salesFS * vehEff.VehRevision.InitAuxiliaryHevCosts.TaxCredit;
                vehEff.InitBatteryTaxCredits = salesFS * vehEff.VehRevision.InitAuxiliaryHevCosts.BatteryTaxCredit;
            }

            //--- ownership costs ---
            if (!vehEff.IsHistoric) {
                this.CalcVehicleOwnershipCosts(age, vehEff.VehStyle,
                    vehEff.InitFS[fuelType], vehEff.Sales, vehEff.MSRP, 0, vehEff.InitOwnershipCosts);
            }

            //--- energy and emissions costs ---
            var fuelProperties = this.Parameters.FuelProperties;
            double fuelPriceCY = this.Parameters.FuelPrices.GetFuelPrice(fuelType, calendarYear, fuelProperties);
            double benefitsScale = this.RuntimeSettings.ConsumerBenefitsScale;
            vehEff.InitRetailFuelCosts = vehEff.InitGallons * fuelPriceCY * benefitsScale;

            var greenhouseCosts = this.Parameters.GreenhouseEmissionCosts;
            vehEff.InitEmCostsCO2 = (vehEff.InitUpstream.CO2.Total + vehEff.InitTailpipe.CO2) *
                greenhouseCosts.GetCO2Costs(Estimates.Average, calendarYear) / 1000;
        }

        /// <summary>
        /// Calculates all undiscounted societal and consumer costs for:
        ///     (1) a vehicle model evaluated and updated during analysis
        ///             -or-
        ///     (2) a representative historic vehicle.
        /// </summary>
        void CalculateVehicleCosts(int year, int age, FT fuelType, VehicleEffects vehEff, IVehicle veh)
        {
            // NOTES:
            //  Some of the "cost" calculations are not applicable for a representative historic
            //  vehicle, and should thus be skipped.
            //  Specifically:
            //      - all tech and ownership costs
            //      - rebound fuel costs
            //      - drive surplus
            //      - refuel surplus (of electric vehicles only)
            //  For a historic vehicle, the supplied 'veh' param will be null and is ignored; for
            //  an analysis vehicle, the 'veh' param will correspond to the actual vehicle.

            //--- tech costs & value losses ---
            double vehFS = vehEff.FS[fuelType];

            if (!vehEff.IsHistoric)
            {
                this.CalcVehicleCostsAndLosses(year, age, vehFS, vehEff.Sales, veh, vehEff);
            }

            //--- ownership costs ---
            if (!vehEff.IsHistoric)
            {
                var regCost = vehEff.VehRevision.ComplianceCosts.RegCost;
                this.CalcVehicleOwnershipCosts(
                    age, vehEff.VehStyle, vehFS, vehEff.Sales, vehEff.MSRP, regCost, vehEff.OwnershipCosts);
            }

            //--- energy costs ---
            this.CalcVehicleEnergyCosts(year, age, fuelType, vehEff, veh);

            //--- costs from market externalities ---
            this.CalcVehicleMarketExternalities(year, age, fuelType, vehEff);

            //--- emissions costs ---
            this.CalcVehicleEmissionCosts(year, age, fuelType, vehEff);
        }

        // calc vehicle costs -- helper methods
        void CalcVehicleCostsAndLosses(
            int year, int age, double vehFS, double vehSales, IVehicle veh, VehicleEffects vehEff)
        {
            // all technology costs are assumed to occur at point of sale (i.e., age zero)
            if (age != 0) {
                return;
            }

            // divide sales by 1000 so that all downstream calcs are represented as $k instead of $-unit
            double salesFS = vehSales * vehFS / 1000;
            var vehCosts = vehEff.VehRevision.ComplianceCosts;

            vehEff.TechCosts   = salesFS * vehCosts.TotalTechCost;
            vehEff.RegCosts    = salesFS * vehCosts.RegCost;
            vehEff.MntRprCosts = salesFS * vehCosts.MaintRepairCost;

            vehEff.TaxCredits        = salesFS * vehEff.VehRevision.AuxiliaryHevCosts.TaxCredit;
            vehEff.BatteryTaxCredits = salesFS * vehEff.VehRevision.AuxiliaryHevCosts.BatteryTaxCredit;

            // calc implicitly opportunity cost, applying discounting as necessary
            if (this.RuntimeSettings.CalcImplicitOpportunityCost)
            {
                var economicValues = this.Parameters.EconomicValues;
                var operatingCosts = economicValues.OperatingCosts[vehEff.VehStyle];

                double commercialShare = this.RuntimeSettings.CommercialShareLD;
                double consumerSalesFS = salesFS * (1 - commercialShare);

                double mfrPB   = veh.Manufacturer.BaselineInfo.PaybackPeriod[vehEff.VehStyle];
                double extPB   = Math.Max(mfrPB, operatingCosts.AverageResaleTime / 12);
                double initCPM = Effects.CalcCPM(year, vehEff.InitOnRoadFE, vehEff.InitFS, this.Settings);
                double vehCPM  = Effects.CalcCPM(year, vehEff.OnRoadFE, vehEff.FS, this.Settings);
                double fsPB    = Effects.CalcFuelSavings(initCPM, vehCPM, mfrPB, vehEff.VehStyle, this.Settings);
                double fsExt   = Effects.CalcFuelSavings(initCPM, vehCPM, extPB, vehEff.VehStyle, this.Settings);
                double oppCost = Math.Max(0, consumerSalesFS * (fsExt - fsPB));

                // save undiscounted -- this is the same for societal and consumer perspectives
                vehEff.OpportunityCostSocietal = oppCost;
                vehEff.OpportunityCostConsumer = oppCost;

                // calc & save pre-discounted oppurtunity costs based on the societal and consumer disc-rates
                for (int i = 0; i < economicValues.DiscountRate.Length; i++)
                {
                    fsExt = Effects.CalcFuelSavings(year, initCPM, vehCPM, extPB, economicValues.DiscountRate[i],
                        year, vehEff.VehStyle, this.Settings);
                    oppCost = Math.Max(0, consumerSalesFS * (fsExt - fsPB));

                    vehEff.DiscOpportunityCostSocietal[i] = oppCost;
                }

                for (int i = 0; i < economicValues.ConsumerDiscRate.Length; i++)
                {
                    fsExt = Effects.CalcFuelSavings(year, initCPM, vehCPM, extPB, economicValues.ConsumerDiscRate[i],
                        year, vehEff.VehStyle, this.Settings);
                    oppCost = Math.Max(0, consumerSalesFS * (fsExt - fsPB));

                    vehEff.DiscOpportunityCostConsumer[i] = oppCost;
                }
            }
        }

        void CalcVehicleOwnershipCosts(int age, VS vehStyle, double vehFS, double vehSales, double vehMSRP,
            double vehRegCost, OwnershipCosts opCosts)
        {
            var operatingCosts = this.Parameters.EconomicValues.OperatingCosts[vehStyle];
            double msrpFS = vehSales * vehFS * (vehMSRP + vehRegCost) / 1000;

            // all taxes and fees occur during the first year
            opCosts.TaxesAndFees = (age == 0) ? msrpFS * operatingCosts.TaxesAndFees : 0;

            // compute financing costs by age up to the term-years
            double financingTerm = operatingCosts.FinancingTerm;
            int    termYears     = (int)Math.Ceiling(financingTerm / 12D);
            if (age < termYears)
            {
                double financingMult = (operatingCosts.FinancingInterest * operatingCosts.ShareFinanced) /
                                       (1 - Math.Pow(1 + operatingCosts.FinancingInterest / 12, -financingTerm)) -
                                       operatingCosts.ShareFinanced / operatingCosts.FinancingTerm * 12;
                opCosts.FinancingCosts = msrpFS * financingMult;
                double partialYear = (financingTerm / 12D - age);
                if (partialYear < 1)
                {
                    opCosts.FinancingCosts *= partialYear;
                }
            }
            else
            {
                opCosts.FinancingCosts = 0;
            }

            // compute insurance costs by age for the lifetime of the vehicle
            opCosts.InsuranceCosts = msrpFS * (0.8 * 0.0183 / Math.Pow(1 + operatingCosts.VehicleDepreciation, age));
        }
        void CalcVehicleEnergyCosts(int year, int age, FT fuelType, VehicleEffects vehEff, IVehicle veh)
        {
            var    economicParams = this.Parameters.EconomicValues.Parameters[vehEff.VehStyle];
            var    fuelProperties = this.Parameters.FuelProperties;
            var    fuelPrices     = this.Parameters.FuelPrices;
            double fuelPriceCY    = fuelPrices.GetFuelPrice(fuelType, year + age, fuelProperties);
            double fuelTaxCY      = fuelPrices.GetFuelTax(fuelType, year + age, fuelProperties);
            double benefitsScale  = this.RuntimeSettings.ConsumerBenefitsScale;

            vehEff.RetailFuelCosts = vehEff.Gallons * fuelPriceCY * benefitsScale;
            vehEff.FuelTaxCosts    = vehEff.Gallons * fuelTaxCY * benefitsScale;
            vehEff.RefuelSurplus   = fuelType.IsGEGFuel()
                                   ? 0
                                   : vehEff.Gallons / (vehEff.FuelCapacity * economicParams.RefuelTankVolume) *
                                     economicParams.GetRefuelingValue(fuelType, vehEff.FuelCapacity) *
                                     economicParams.RefuelEventsScale;

            if (!vehEff.IsHistoric)
            {
                vehEff.ReboundFuelCosts = (vehEff.VMT - vehEff.VMTNoRebound) *  vehEff.CPM * benefitsScale;
                vehEff.DriveSurplus     = (vehEff.VMT - vehEff.VMTNoRebound) * (vehEff.HistCPM + vehEff.CPM) / 2;

                // for analysis vehicles only, if a given vehicle is BEV200 - BEV500,
                // calculate refuel surplus specific to electricity
                var techKey = vehEff.VehRevision.TechKey;
                EVRechargeThresholds evThresholds =
                    (techKey[TechnologyIndexes.BEV1]) ? economicParams.BEV200Thresholds :
                    (techKey[TechnologyIndexes.BEV2]) ? economicParams.BEV300Thresholds :
                    (techKey[TechnologyIndexes.BEV3]) ? economicParams.BEV400Thresholds :
                    (techKey[TechnologyIndexes.BEV4]) ? economicParams.BEV500Thresholds : null;

                if (evThresholds != null)
                {
                    double rechargeEvents = vehEff.VMT / evThresholds.ChargeFrequency;
                    double milesRecharged = vehEff.VMT * evThresholds.ShareCharged;
                    double rechargeTime   = economicParams.RefuelTime.Electricity * rechargeEvents / 60 +
                                            milesRecharged / evThresholds.ChargeRate;
                    vehEff.RefuelSurplus  = rechargeTime * economicParams.VehicleTravelValue;
                }
            }
        }
        void CalcVehicleMarketExternalities(int year, int age, FT fuelType, VehicleEffects vehEff)
        {
            var    externalCosts = this.Parameters.EconomicValues.ExternalCosts[vehEff.VehStyle];
            double economicCosts = this.Parameters.EconomicValues.EconomicCosts.GetTotalEconomicCosts(year + age);
            double shareOfDomRef = this.Parameters.FuelImportAssumptions[fuelType, year + age].GetShareOfDomesticRefining();

            vehEff.EconomicCosts   = vehEff.Gallons * economicCosts * shareOfDomRef;
            vehEff.CongestionCosts = vehEff.VMT     * externalCosts.Congestion;
            vehEff.NoiseCosts      = vehEff.VMT     * externalCosts.Noise;
        }

        void CalcVehicleEmissionCosts(int year, int age, FT fuelType, VehicleEffects vehEff)
        {
            this.CalcVehicleCriteriaEmissionCosts(year, age, fuelType, vehEff);
            this.CalcVehicleGHGEmissionCosts(year, age, vehEff);
        }
        void CalcVehicleCriteriaEmissionCosts(int year, int age, FT fuelType, VehicleEffects vehEff)
        {
            var criteriaCosts = this.Parameters.CriteriaEmissionCosts;
            int calendarYear = year + age;
            var emCosts = vehEff.EmCosts;

            this.CalcVehicleCriteriaEmissionCosts(fuelType, vehEff, criteriaCosts.DR3[calendarYear],
                                                  out double noxDR3, out double so2DR3, out double pmDR3);
            this.CalcVehicleCriteriaEmissionCosts(fuelType, vehEff, criteriaCosts.DR7[calendarYear],
                                                  out double noxDR7, out double so2DR7, out double pmDR7);

            emCosts.NOx.DR3 = noxDR3;
            emCosts.SO2.DR3 = so2DR3;
            emCosts.PM.DR3 = pmDR3;

            emCosts.NOx.DR7 = noxDR7;
            emCosts.SO2.DR7 = so2DR7;
            emCosts.PM.DR7 = pmDR7;

            // we don't currently compute costs for CO and VOC
            emCosts.CO = 0;
            emCosts.VOC = 0;
        }
        void CalcVehicleCriteriaEmissionCosts(
            FT fuelType, VehicleEffects vehEff, CriteriaEmissionsValues criteriaCosts,
            out double emCostsNOx, out double emCostsSO2, out double emCostsPM)
        {
            // criteria costs for upstream emissions
            var upstream = vehEff.Upstream;
            double upstreamCostsNOX;
            double upstreamCostsSO2;
            double upstreamCostsPM;

            if (fuelType.IsGEGFuel())
            {
                upstreamCostsNOX = upstream.NOx.Total * criteriaCosts.UpstreamElectricity.NOX;
                upstreamCostsSO2 = upstream.SO2.Total * criteriaCosts.UpstreamElectricity.SO2;
                upstreamCostsPM  = upstream.PM .Total * criteriaCosts.UpstreamElectricity.PM;
            }
            else
            {
                upstreamCostsNOX =
                    upstream.NOx.Refining       * criteriaCosts.UpstreamRefineries.NOX +
                    upstream.NOx.Extraction     * criteriaCosts.UpstreamExtraction.NOX +
                    upstream.NOx.Transportation * criteriaCosts.UpstreamTransportation.NOX +
                    upstream.NOx.FuelTSD        * criteriaCosts.UpstreamFuelTSD.NOX;
                upstreamCostsSO2 =
                    upstream.SO2.Refining       * criteriaCosts.UpstreamRefineries.SO2 +
                    upstream.SO2.Extraction     * criteriaCosts.UpstreamExtraction.SO2 +
                    upstream.SO2.Transportation * criteriaCosts.UpstreamTransportation.SO2 +
                    upstream.SO2.FuelTSD        * criteriaCosts.UpstreamFuelTSD.SO2;
                upstreamCostsPM =
                    upstream.PM.Refining        * criteriaCosts.UpstreamRefineries.PM +
                    upstream.PM.Extraction      * criteriaCosts.UpstreamExtraction.PM +
                    upstream.PM.Transportation  * criteriaCosts.UpstreamTransportation.PM +
                    upstream.PM.FuelTSD         * criteriaCosts.UpstreamFuelTSD.PM;
            }

            // criteria costs for downstream emissions
            CriteriaEmissionsValue downstreamSources = null;
            if (fuelType == FT.Diesel)
            {
                downstreamSources = vehEff.VehClass.IsLDT2b3()
                                  ? criteriaCosts.DownstreamDsl2b3
                                  : criteriaCosts.DownstreamDslLDx;
            }
            else if (fuelType == FT.Gasoline || fuelType == FT.Ethanol85)
            {
                downstreamSources = vehEff.VehClass.IsLDT2b3() ? criteriaCosts.DownstreamGas2b3
                                  : vehEff.VehClass.IsLDT12a() ? criteriaCosts.DownstreamGasLDT
                                  :                              criteriaCosts.DownstreamGasLDV;
            }

            var tailpipe = vehEff.Tailpipe;
            double tailpipeCostsNOX = 0;
            double tailpipeCostsSO2 = 0;
            double tailpipeCostsPM  = 0;

            if (downstreamSources != null)
            {
                tailpipeCostsNOX = tailpipe.NOx * downstreamSources.NOX;
                tailpipeCostsSO2 = tailpipe.SO2 * downstreamSources.SO2;
                tailpipeCostsPM  = (tailpipe.PM + vehEff.BrakeTireWearPM) * downstreamSources.PM;
            }

            // combine upstream and tailpipe costs
            emCostsNOx = MetricToShortTon * (upstreamCostsNOX + tailpipeCostsNOX) / 1000;
            emCostsSO2 = MetricToShortTon * (upstreamCostsSO2 + tailpipeCostsSO2) / 1000;
            emCostsPM  = MetricToShortTon * (upstreamCostsPM  + tailpipeCostsPM ) / 1000;
        }
        void CalcVehicleGHGEmissionCosts(int year, int age, VehicleEffects vehEff)
        {
            double co2TotalEmissions = (vehEff.Upstream.CO2.Total + vehEff.Tailpipe.CO2);
            double ch4TotalEmissions = (vehEff.Upstream.CH4.Total + vehEff.Tailpipe.CH4);
            double n2oTotalEmissions = (vehEff.Upstream.N2O.Total + vehEff.Tailpipe.N2O);

            int calendarYear = year + age;
            var ghgCosts = this.Parameters.GreenhouseEmissionCosts;
            var emCosts = vehEff.EmCosts;

            foreach (var ghgEstimates in GhgEstimates.List)
            {
                emCosts.CO2[ghgEstimates] = co2TotalEmissions * ghgCosts.GetCO2Costs(ghgEstimates, calendarYear) / 1000;
                emCosts.CH4[ghgEstimates] = ch4TotalEmissions * ghgCosts.GetCH4Costs(ghgEstimates, calendarYear) / 1000;
                emCosts.N2O[ghgEstimates] = n2oTotalEmissions * ghgCosts.GetN2OCosts(ghgEstimates, calendarYear) / 1000;
            }
        }

        /// <summary>
        /// Calculates related reallocated VMT surplus for a vehicle model evaluated and updated during analysis.
        /// This calculation **should not** apply to historic vehicles.
        /// </summary>
        void CalculateVehicleReallocatedVmtSurplus(Scenario scen, int year, int age, VehicleEffects vehEff,
            BaselineVehicleEffects baseVehEff, ModelingEffectsItem annualEff)
        {
            if (scen.IsBaseline || vehEff.IsHistoric || vehEff.Sales == 0 || baseVehEff.Sales == 0) {
                return;
            }

            // compute reallocated VMT surplus based on **unit** baseline & scenario vehicle
            double scenCPM = vehEff.CPM;
            double scenVMTRealloc = vehEff.VMTReallocTotal / vehEff.FleetTotal;
            double baseVMTRealloc = baseVehEff.VMTReallocated[age].Total / baseVehEff.Fleet[age].Total;
            double reallocVmtSurplus = (scenVMTRealloc - baseVMTRealloc) * scenCPM;
            double reallocVmtSurplusTotal = reallocVmtSurplus * vehEff.FleetTotal;

            // apportion by fuel type and aggregate
            foreach (FT fuelType in FuelTypes.List)
            {
                double vehFS = vehEff.FS[fuelType];
                if (vehFS == 0) {
                    continue;
                }

                double reallocVmtSurplusFS = vehFS * reallocVmtSurplusTotal;
                annualEff.ConsumerCosts[vehEff.RCIndex].ReallocatedVMTSurplus += reallocVmtSurplusFS;
            }
        }

        /// <summary>
        /// Calculates vehicle related fatalities and undiscounted costs for:
        ///     (1) a vehicle model evaluated and updated during analysis
        ///             -or-
        ///     (2) a representative historic vehicle.
        /// </summary>
        void CalculateVehicleFatalities(Scenario scen, int year, int age, VehicleEffects vehEff,
            BaselineVehicleEffects baseVehEff, ModelingEffectsItem annualEff)
        {
            var safetyValues = this.Parameters.SafetyValues;
            var fatalityRates = this.Parameters.FatalityRates;
            var fatalityEstimates = this.RuntimeSettings.FatalityRateEstimates;

            // calculate weight effect (change per 100 lbs)
            double scenFatalEffect = 0;
            double initFatalEffect = 0;
            if (vehEff.SftClass != SC.None)
            {
                double refWeight = safetyValues.Threshold[vehEff.SftClass];
                double vehCW     = vehEff.CurbWeight;
                scenFatalEffect  = safetyValues.GetEffect(vehEff.SftClass, vehCW) * (refWeight - vehCW) / 100;

                double initCW   = vehEff.InitCW;
                initFatalEffect = safetyValues.GetEffect(vehEff.SftClass, initCW) * (refWeight - initCW) / 100;
            }

            // calculate fatality/non-fatal totals
            double fatalityRate = fatalityRates.GetFatalityRate(fatalityEstimates, year, age) / 1e6;
            double fatalitiesTotal = vehEff.VMTTotal * fatalityRate * (scenFatalEffect + 1);
            double fatalitiesReboundTotal = 0;
            double fatalitiesDeltaCWTotal = 0;

            double nonFatalInjuryRate = fatalityRates.GetNonFatalInjuryRate(fatalityEstimates, year, age) / 1e6;
            double nonFatalInjuriesTotal = vehEff.VMTTotal * nonFatalInjuryRate * (scenFatalEffect + 1);
            double nonFatalInjuriesReboundTotal = 0;
            double nonFatalInjuriesDeltaCWTotal = 0;

            double propertyDamageCrashRate = fatalityRates.GetPropertyDamageCrashRate(fatalityEstimates, year, age) / 1e6;
            double propertyDamageCrashesTotal = vehEff.VMTTotal * propertyDamageCrashRate * (scenFatalEffect + 1);
            double propertyDamageCrashesReboundTotal = 0;
            double propertyDamageCrashesDeltaCWTotal = 0;

            double vmtReboundOnlyEffect = vehEff.VMTReboundOnlyTotal * (scenFatalEffect + 1);
            fatalitiesReboundTotal = vmtReboundOnlyEffect * fatalityRate;
            nonFatalInjuriesReboundTotal = vmtReboundOnlyEffect * nonFatalInjuryRate;
            propertyDamageCrashesReboundTotal = vmtReboundOnlyEffect * propertyDamageCrashRate;

            double baseVMTNoRebound = (scen.IsBaseline) ? vehEff.VMTNoReboundTotal : baseVehEff.VMTNoRebound[age].Total;
            double deltaFatalEffect = baseVMTNoRebound * (scenFatalEffect - initFatalEffect);
            fatalitiesDeltaCWTotal = deltaFatalEffect * fatalityRate;
            nonFatalInjuriesDeltaCWTotal = deltaFatalEffect * nonFatalInjuryRate;
            propertyDamageCrashesDeltaCWTotal = deltaFatalEffect * propertyDamageCrashRate;

            // apportion fatalities by fuel type, calc fatality costs, and aggregate
            foreach (FT fuelType in FuelTypes.List)
            {
                double vehFS = vehEff.FS[fuelType];

                if (vehFS != 0)
                {
                    // fatality values (weighted by fuel-share)
                    double fatalities        = vehFS * fatalitiesTotal;
                    double fatalitiesRebound = vehFS * fatalitiesReboundTotal;
                    double fatalitiesDeltaCW = vehFS * fatalitiesDeltaCWTotal;

                    double nonFatalInjuries        = vehFS * nonFatalInjuriesTotal;
                    double nonFatalInjuriesRebound = vehFS * nonFatalInjuriesReboundTotal;
                    double nonFatalInjuriesDeltaCW = vehFS * nonFatalInjuriesDeltaCWTotal;

                    double propertyDamageCrashes        = vehFS * propertyDamageCrashesTotal;
                    double propertyDamageCrashesRebound = vehFS * propertyDamageCrashesReboundTotal;
                    double propertyDamageCrashesDeltaCW = vehFS * propertyDamageCrashesDeltaCWTotal;

                    // fatality costs
                    double baseFatalityCosts    = safetyValues.GetFatalityCosts(year + age) / 1000; // convert to $k
                    double fatalityCosts        = baseFatalityCosts * fatalities;
                    double fatalityCostsRebound = baseFatalityCosts * fatalitiesRebound;
                    double fatalityCostsDeltaCW = baseFatalityCosts * fatalitiesDeltaCW;

                    double baseNonFatalInjuryCosts    = safetyValues.GetNonFatalInjuryCosts(year + age) / 1000; // convert to $k
                    double nonFatalInjuryCosts        = baseNonFatalInjuryCosts * nonFatalInjuries;
                    double nonFatalInjuryCostsRebound = baseNonFatalInjuryCosts * nonFatalInjuriesRebound;
                    double nonFatalInjuryCostsDeltaCW = baseNonFatalInjuryCosts * nonFatalInjuriesDeltaCW;

                    double basePropertyDamageCosts    = safetyValues.GetPropertyDamageCrashCosts(year + age) / 1000; // convert to $k
                    double propertyDamageCosts        = basePropertyDamageCosts * propertyDamageCrashes;
                    double propertyDamageCostsRebound = basePropertyDamageCosts * propertyDamageCrashesRebound;
                    double propertyDamageCostsDeltaCW = basePropertyDamageCosts * propertyDamageCrashesDeltaCW;

                    double fatalityRiskValue = safetyValues.FatalityRisk * fatalityCostsRebound;
                    double nonFatalRiskValue = safetyValues.FatalityRisk * (nonFatalInjuryCostsRebound + propertyDamageCostsRebound);

                    // aggregate fatalities
                    int rc = vehEff.RCIndex;
                    int ft = fuelType.GetListIndex();
                    var safetyEffects = annualEff.SafetyEffects[rc, ft];
                    var socialCosts = annualEff.SocialCosts[rc, ft];

                    // safety effects (fatalities)
                    safetyEffects.FatalitiesOverall += fatalities;
                    safetyEffects.FatalitiesRebound += fatalitiesRebound;
                    safetyEffects.FatalitiesDeltaCW += fatalitiesDeltaCW;
                    safetyEffects.NonFatalInjuriesOverall += nonFatalInjuries;
                    safetyEffects.NonFatalInjuriesRebound += nonFatalInjuriesRebound;
                    safetyEffects.NonFatalInjuriesDeltaCW += nonFatalInjuriesDeltaCW;
                    safetyEffects.PropertyDamageCrashesOverall += propertyDamageCrashes;
                    safetyEffects.PropertyDamageCrashesRebound += propertyDamageCrashesRebound;
                    safetyEffects.PropertyDamageCrashesDeltaCW += propertyDamageCrashesDeltaCW;

                    // safety costs
                    socialCosts.FatalityCostsOverall += fatalityCosts;
                    socialCosts.FatalityCostsRebound += fatalityCostsRebound;
                    socialCosts.FatalityCostsDeltaCW += fatalityCostsDeltaCW;
                    socialCosts.NonFatalInjuryCostsOverall += nonFatalInjuryCosts;
                    socialCosts.NonFatalInjuryCostsRebound += nonFatalInjuryCostsRebound;
                    socialCosts.NonFatalInjuryCostsDeltaCW += nonFatalInjuryCostsDeltaCW;
                    socialCosts.PropertyDamageCostsOverall += propertyDamageCosts;
                    socialCosts.PropertyDamageCostsRebound += propertyDamageCostsRebound;
                    socialCosts.PropertyDamageCostsDeltaCW += propertyDamageCostsDeltaCW;

                    // "derived" safety costs
                    socialCosts.FatalityRiskValue += fatalityRiskValue;
                    socialCosts.NonFatalRiskValue += nonFatalRiskValue;
                }
            }
        }

        /// <summary>
        /// Aggregates a subset of undiscounted societal and consumer costs applicable to an "initial" vehicle.
        /// NOTE: while some effects are calculated for an "initial" vehicle, they are presently not aggregated.
        /// </summary>
        void AggregateInitVehEffectsAndCosts(FT fuelType, VehicleEffects vehEff, ModelingEffectsItem annualEff)
        {
            int rc = vehEff.RCIndex;

            //--- undiscounted social costs ---
            var socialCosts = annualEff.SocialCosts[rc, fuelType.GetListIndex()];

            //--- energy costs ---
            socialCosts.InitRetailFuelCosts += vehEff.InitRetailFuelCosts;

            //--- undiscounted consumer costs ---
            var consumerCosts = annualEff.ConsumerCosts[rc];
            consumerCosts.InitTaxCredits += vehEff.InitTaxCredits;
            consumerCosts.InitBatteryTaxCredits += vehEff.InitBatteryTaxCredits;
            consumerCosts.InitOwnershipCosts.TaxesAndFees += vehEff.InitOwnershipCosts.TaxesAndFees;
            consumerCosts.InitOwnershipCosts.FinancingCosts += vehEff.InitOwnershipCosts.FinancingCosts;
            consumerCosts.InitOwnershipCosts.InsuranceCosts += vehEff.InitOwnershipCosts.InsuranceCosts;
            consumerCosts.InitialRetailFuelCosts += vehEff.InitRetailFuelCosts;
        }
        /// <summary>
        /// Aggregates all effects, undiscounted societal costs and undiscounted consumer costs for:
        ///     (1) a vehicle model evaluated and updated during analysis
        ///             -or-
        ///     (2) a representative historic vehicle.
        /// </summary>
        void AggregateVehicleEffectsAndCosts(
            int year, int age, FT fuelType, VehicleEffects vehEff, ModelingEffectsItem annualEff)
        {
            var economicValues = this.Parameters.EconomicValues;
            int rc = vehEff.RCIndex;
            int ft = fuelType.GetListIndex();

            //--- energy effects ---
            var energyEffects = annualEff.EnergyEffects[rc, ft];

            energyEffects.Fleet        += vehEff.Fleet;
            energyEffects.VMT          += vehEff.VMT;
            energyEffects.VMTNoRebound += vehEff.VMTNoRebound;
            energyEffects.BTU          += vehEff.BTU;
            energyEffects.Gallons      += vehEff.Gallons;
            energyEffects.RatedGallons += vehEff.RatedGallons;
            energyEffects.NativeUnits  += vehEff.EnergyUse;

            //--- safety effects (emission health impacts) ---
            var safetyEffects = annualEff.SafetyEffects[rc, ft];
            for (int i = 0; i < vehEff.UpstreamEHI.Length; i++) {
                safetyEffects.UpstreamHealthImpacts[i] += vehEff.UpstreamEHI[i];
                safetyEffects.TailpipeHealthImpacts[i] += vehEff.TailpipeEHI[i];
            }

            //--- emission effects ---
            var emissionEffects = annualEff.EmissionEffects[rc, ft];
            foreach (var pollutant in Pollutants.List) {
                emissionEffects.Upstream[pollutant] += vehEff.Upstream[pollutant].Total;
                emissionEffects.Tailpipe[pollutant] += vehEff.Tailpipe[pollutant];
            }

            // break and tire wear are only defined for PM2.5
            emissionEffects.BrakeTireWearPM += vehEff.BrakeTireWearPM;

            //--- undiscounted social costs ---
            var socialCosts = annualEff.SocialCosts[rc, ft];

            //--- tech costs & value losses ---
            if (age == 0 && !vehEff.IsHistoric)
            {
                socialCosts.TechCosts += vehEff.TechCosts;
                socialCosts.MaintRepairCosts += vehEff.MntRprCosts;

                // NOTE: OpportunityCost is a special case and has to be discounted during aggregation
                socialCosts.OpportunityCost += vehEff.OpportunityCostSocietal;

                for (int i = 0; i < economicValues.DiscountRate.Length; i++)
                {
                    var discSocialCosts = annualEff.DiscSocialCosts[i][rc, ft];
                    double socialDR = Effects.GetDiscRateMultiplier(year, age, economicValues.DRBaseYear, economicValues.DiscountRate[i]);
                    double discOpportunityCost = vehEff.DiscOpportunityCostSocietal[i] * socialDR;

                    discSocialCosts.OpportunityCost += discOpportunityCost;
                }
            }

            //--- energy costs ---
            socialCosts.FuelTaxCosts    += vehEff.FuelTaxCosts;
            socialCosts.RetailFuelCosts += vehEff.RetailFuelCosts;
            socialCosts.DriveSurplus    += vehEff.DriveSurplus;
            socialCosts.RefuelSurplus   += vehEff.RefuelSurplus;

            //--- costs from market externalities ---
            socialCosts.EconomicCosts   += vehEff.EconomicCosts;
            socialCosts.CongestionCosts += vehEff.CongestionCosts;
            socialCosts.NoiseCosts      += vehEff.NoiseCosts;

            //--- emissions costs ---
            // criteria pollutants
            socialCosts.EmissionsCostsCO += vehEff.EmCosts.CO;
            socialCosts.EmissionsCostsVOC += vehEff.EmCosts.VOC;

            // criteria pollutants pre-discounted at 3% or 7%
            socialCosts.EmissionsCostsNOx.Append(vehEff.EmCosts.NOx);
            socialCosts.EmissionsCostsSO2.Append(vehEff.EmCosts.SO2);
            socialCosts.EmissionsCostsPM .Append(vehEff.EmCosts.PM);

            // ghg pollutants at various estimates (low, average, high, very high)
            socialCosts.EmissionsCostsCO2.Append(vehEff.EmCosts.CO2);
            socialCosts.EmissionsCostsCH4.Append(vehEff.EmCosts.CH4);
            socialCosts.EmissionsCostsN2O.Append(vehEff.EmCosts.N2O);

            //--- undiscounted consumer costs ---
            if (!vehEff.IsHistoric)
            {
                var consumerCosts = annualEff.ConsumerCosts[rc];

                //--- tech costs ---
                if (age == 0)
                {
                    consumerCosts.Sales             += vehEff.Sales * vehEff.FS[fuelType];
                    consumerCosts.TechCosts         += vehEff.TechCosts;
                    consumerCosts.RegCosts          += vehEff.RegCosts;
                    consumerCosts.TaxCredits        += vehEff.TaxCredits;
                    consumerCosts.BatteryTaxCredits += vehEff.BatteryTaxCredits;
                    consumerCosts.MaintRepairCosts  += vehEff.MntRprCosts;

                    // NOTE: OpportunityCost is a special case and has to be discounted during aggregation
                    consumerCosts.OpportunityCost += vehEff.OpportunityCostConsumer;

                    for (int i = 0; i < economicValues.ConsumerDiscRate.Length; i++)
                    {
                        var discConsumerCosts = annualEff.DiscConsumerCosts[i][rc];
                        double consumerDR = Effects.GetDiscRateMultiplier(year, age, 0, economicValues.ConsumerDiscRate[i]);
                        double discOpportunityCost = vehEff.DiscOpportunityCostConsumer[i] * consumerDR;

                        discConsumerCosts.OpportunityCost += discOpportunityCost;
                    }
                }

                //--- ownership costs ---
                consumerCosts.OwnershipCosts.TaxesAndFees   += vehEff.OwnershipCosts.TaxesAndFees;
                consumerCosts.OwnershipCosts.FinancingCosts += vehEff.OwnershipCosts.FinancingCosts;
                consumerCosts.OwnershipCosts.InsuranceCosts += vehEff.OwnershipCosts.InsuranceCosts;

                //--- energy costs ---
                consumerCosts.RetailFuelCosts  += vehEff.RetailFuelCosts;
                consumerCosts.ReboundFuelCosts += vehEff.ReboundFuelCosts;
                consumerCosts.DriveSurplus     += vehEff.DriveSurplus;
                consumerCosts.RefuelSurplus    += vehEff.RefuelSurplus;
            }
        }

        void CalcCommercialOppCost(Scenario scen, int year, EffectsData.ModelingEffects ed)
        {
            //------------------------------------------------------------------------------------
            // NOTES:
            //
            // Commercial operator implicit opportunity cost needs to be computed at the end of
            // effects calculations, once all other values have been computed for all vehicle ages
            // (but before the annual data is aggregated to the annual totals). We need to do this
            // at the end because of how some components of commercial opp-cost are computed.
            // 
            // Commercial opp-cost is inheritly incremental. Thus, for the baseline scenario, we
            // only compute and save it temporarily so that we can calculate an incremental in the
            // alternatives.
            //------------------------------------------------------------------------------------
            if (!this.RuntimeSettings.CalcCommercialOpportunityCost) {
                return;
            }

            // calculate for current year
            var scenCommercialOppCost = this.CalcCommercialOppCost(ed);

            if (scen.IsBaseline) {
                this._baseCommercialOppCost.Add(year, scenCommercialOppCost);
                return;
            }

            var discRateCount = this.Parameters.EconomicValues.DiscountRate.Length + 1;
            var baseCommercialOppCost = this._baseCommercialOppCost[year];

            for (int age = 0; age < Globals.CY; age++) {
                var annualED = ed.AnnualEffects[age];

                for (int drIndex = 0; drIndex < discRateCount; drIndex++) {
                    // drIndex:  0 = undiscounted; 1..n = discounted
                    var socCosts = (drIndex == 0)
                                    ? annualED.SocialCosts
                                    : annualED.DiscSocialCosts[drIndex - 1];

                    for (int rc = 0; rc < RegClasses.AltList.Length; rc++) {
                        for (int ft = 0; ft < FuelTypes.List.Length; ft++) {
                            FT fuelType = FuelTypes.List[ft];
                            socCosts[rc, ft].CommercialOppCost = scenCommercialOppCost[age][drIndex].Items[rc][fuelType]
                                                               - baseCommercialOppCost[age][drIndex].Items[rc][fuelType];
                        }
                    }
                }
            }
        }

        RCAltFuelValue[][] CalcCommercialOppCost(EffectsData.ModelingEffects ed)
        {
            var discRateCount = this.Parameters.EconomicValues.DiscountRate.Length + 1;

            // values indexed by: age, disc-rate, RC, FT
            RCAltFuelValue[][] values = new RCAltFuelValue[Globals.CY][];
            for (int i = 0; i < values.Length; i++) {
                values[i] = Enumerable.Range(0, discRateCount).Select(_ => new RCAltFuelValue()).ToArray();
            }

            for (int rc = 0; rc < RegClasses.AltList.Length; rc++) {
                RC regClass = RegClasses.AltList[rc];
                double commercialShare = this.RuntimeSettings.CommercialShareLD;

                for (int ft = 0; ft < FuelTypes.List.Length; ft++) {
                    FT fuelType = FuelTypes.List[ft];

                    for (int age = 0; age < Globals.CY; age++) {
                        var annualED = ed.AnnualEffects[age];

                        for (int drIndex = 0; drIndex < discRateCount; drIndex++) {
                            // drIndex:  0 = undiscounted; 1..n = discounted
                            var socCosts = (drIndex == 0)
                                         ? annualED.SocialCosts[rc, ft]
                                         : annualED.DiscSocialCosts[drIndex - 1][rc, ft];

                            double benefits = socCosts.DriveSurplus
                                            - socCosts.RefuelSurplus
                                            - socCosts.RetailFuelCosts;
                            double costs = socCosts.TechCosts
                                         + socCosts.MaintRepairCosts
                                         + socCosts.OpportunityCost
                                         + socCosts.NewSalesSurplus;

                            double commercialOppCost = (benefits - costs) * commercialShare;
                            values[age][drIndex].Items[rc][fuelType] = commercialOppCost;
                        }
                    }
                }
            }

            return values;
        }

        void CalculateDiscountedSocialCosts(int year, EffectsData.ModelingEffects ed)
        {
            var economicValues = this.Parameters.EconomicValues;
            double[] co2DiscRates = new double[GhgEstimates.List.Length];

            for (int i = 0; i < economicValues.DiscountRate.Length; i++)
            {
                for (int age = 0; age < Globals.CY; age++)
                {
                    double socialDiscRate = Effects.GetDiscRateMultiplier(
                        year, age, economicValues.DRBaseYear, economicValues.DiscountRate[i]);

                    for (int j = 0; j < co2DiscRates.Length; j++)
                    {
                        double co2Dr = economicValues.GetCO2DiscountRate(GhgEstimates.List[j]);
                        co2DiscRates[j] = Effects.GetDiscRateMultiplier(year, age, economicValues.DRBaseYear, co2Dr);
                    }

                    var annualED = ed.AnnualEffects[age];
                    var undiscCosts = annualED.SocialCosts;
                    var discCosts = annualED.DiscSocialCosts[i];

                    for (int rc = 0; rc < RegClasses.AltList.Length; rc++) {
                        for (int ft = 0; ft < FuelTypes.List.Length; ft++) {
                            this.CalculateDiscountedSocialCosts(
                                undiscCosts[rc, ft], discCosts[rc, ft], socialDiscRate, co2DiscRates);
                        }
                    }
                }
            }
        }
        void CalculateDiscountedSocialCosts(SocietalCosts undiscCosts, SocietalCosts discCosts,
            double socialDiscRate, double[] co2DiscRates)
        {
            // general costs
            discCosts.NewSalesSurplus = undiscCosts.NewSalesSurplus * socialDiscRate;

            // tech costs & value losses
            discCosts.TechCosts        = undiscCosts.TechCosts        * socialDiscRate;
            discCosts.MaintRepairCosts = undiscCosts.MaintRepairCosts * socialDiscRate;
            // NOTE: OpportunityCost is a special case and had to be discounted during aggregation

            // energy costs
            discCosts.InitRetailFuelCosts = undiscCosts.InitRetailFuelCosts * socialDiscRate;
            discCosts.FuelTaxCosts        = undiscCosts.FuelTaxCosts        * socialDiscRate;
            discCosts.RetailFuelCosts     = undiscCosts.RetailFuelCosts     * socialDiscRate;
            discCosts.DriveSurplus        = undiscCosts.DriveSurplus        * socialDiscRate;
            discCosts.RefuelSurplus       = undiscCosts.RefuelSurplus       * socialDiscRate;

            // costs from market externalities
            discCosts.EconomicCosts   = undiscCosts.EconomicCosts   * socialDiscRate;
            discCosts.CongestionCosts = undiscCosts.CongestionCosts * socialDiscRate;
            discCosts.NoiseCosts      = undiscCosts.NoiseCosts      * socialDiscRate;

            // safety costs
            discCosts.FatalityCostsOverall = undiscCosts.FatalityCostsOverall * socialDiscRate;
            discCosts.FatalityCostsRebound = undiscCosts.FatalityCostsRebound * socialDiscRate;
            discCosts.FatalityCostsDeltaCW = undiscCosts.FatalityCostsDeltaCW * socialDiscRate;

            discCosts.NonFatalInjuryCostsOverall = undiscCosts.NonFatalInjuryCostsOverall * socialDiscRate;
            discCosts.NonFatalInjuryCostsRebound = undiscCosts.NonFatalInjuryCostsRebound * socialDiscRate;
            discCosts.NonFatalInjuryCostsDeltaCW = undiscCosts.NonFatalInjuryCostsDeltaCW * socialDiscRate;

            discCosts.PropertyDamageCostsOverall = undiscCosts.PropertyDamageCostsOverall * socialDiscRate;
            discCosts.PropertyDamageCostsRebound = undiscCosts.PropertyDamageCostsRebound * socialDiscRate;
            discCosts.PropertyDamageCostsDeltaCW = undiscCosts.PropertyDamageCostsDeltaCW * socialDiscRate;

            // "derived" safety costs
            discCosts.FatalityRiskValue = undiscCosts.FatalityRiskValue * socialDiscRate;
            discCosts.NonFatalRiskValue = undiscCosts.NonFatalRiskValue * socialDiscRate;

            // emission costs
            // criteria pollutants
            discCosts.EmissionsCostsCO  = undiscCosts.EmissionsCostsCO  * socialDiscRate;
            discCosts.EmissionsCostsVOC = undiscCosts.EmissionsCostsVOC * socialDiscRate;

            // criteria pollutants pre-discounted at 3%
            discCosts.EmissionsCostsNOx.DR3 = undiscCosts.EmissionsCostsNOx.DR3 * socialDiscRate;
            discCosts.EmissionsCostsSO2.DR3 = undiscCosts.EmissionsCostsSO2.DR3 * socialDiscRate;
            discCosts.EmissionsCostsPM .DR3 = undiscCosts.EmissionsCostsPM .DR3 * socialDiscRate;

            // criteria pollutants pre-discounted at 7%
            discCosts.EmissionsCostsNOx.DR7 = undiscCosts.EmissionsCostsNOx.DR7 * socialDiscRate;
            discCosts.EmissionsCostsSO2.DR7 = undiscCosts.EmissionsCostsSO2.DR7 * socialDiscRate;
            discCosts.EmissionsCostsPM .DR7 = undiscCosts.EmissionsCostsPM .DR7 * socialDiscRate;

            // ghg pollutants at various estimates (low, average, high, very high)
            for (int i = 0; i < GhgEstimates.List.Length; i++) {
                Estimates estimates = GhgEstimates.List[i];

                discCosts.EmissionsCostsCO2[estimates] = undiscCosts.EmissionsCostsCO2[estimates] * co2DiscRates[i];
                discCosts.EmissionsCostsCH4[estimates] = undiscCosts.EmissionsCostsCH4[estimates] * co2DiscRates[i];
                discCosts.EmissionsCostsN2O[estimates] = undiscCosts.EmissionsCostsN2O[estimates] * co2DiscRates[i];
            }
        }

        void CalculateDiscountedConsumerCosts(int year, EffectsData.ModelingEffects effData)
        {
            var economicValues = this.Parameters.EconomicValues;

            for (int i = 0; i < economicValues.ConsumerDiscRate.Length; i++)
            {
                for (int age = 0; age < Globals.CY; age++)
                {
                    var annualEff = effData.AnnualEffects[age];
                    var undiscCosts = annualEff.ConsumerCosts;
                    var discCosts = annualEff.DiscConsumerCosts[i];

                    double consumerDR = Effects.GetDiscRateMultiplier(year, age, 0, economicValues.ConsumerDiscRate[i]);

                    // multiply costs by the disc-rate for each reg-class
                    for (int rc = 0; rc < RegClasses.AltList.Length; rc++)
                    {
                        this.CalculateDiscountedConsumerCosts(undiscCosts[rc], discCosts[rc], consumerDR);
                    }
                }
            }
        }
        void CalculateDiscountedConsumerCosts(ConsumerCosts undiscCosts, ConsumerCosts discCosts, double consumerDiscRate)
        {
            // general
            discCosts.Sales           = undiscCosts.Sales;
            discCosts.NewSalesSurplus = undiscCosts.NewSalesSurplus * consumerDiscRate;

            // tech costs & value losses
            discCosts.TechCosts        = undiscCosts.TechCosts        * consumerDiscRate;
            discCosts.RegCosts         = undiscCosts.RegCosts         * consumerDiscRate;
            discCosts.MaintRepairCosts = undiscCosts.MaintRepairCosts * consumerDiscRate;
            // NOTE: OpportunityCost is a special case and had to be discounted during aggregation

            discCosts.InitTaxCredits        = undiscCosts.InitTaxCredits        * consumerDiscRate;
            discCosts.TaxCredits            = undiscCosts.TaxCredits            * consumerDiscRate;
            discCosts.InitBatteryTaxCredits = undiscCosts.InitBatteryTaxCredits * consumerDiscRate;
            discCosts.BatteryTaxCredits     = undiscCosts.BatteryTaxCredits     * consumerDiscRate;

            // ownership and operating costs
            discCosts.InitOwnershipCosts.TaxesAndFees   = undiscCosts.InitOwnershipCosts.TaxesAndFees   * consumerDiscRate;
            discCosts.InitOwnershipCosts.FinancingCosts = undiscCosts.InitOwnershipCosts.FinancingCosts * consumerDiscRate;
            discCosts.InitOwnershipCosts.InsuranceCosts = undiscCosts.InitOwnershipCosts.InsuranceCosts * consumerDiscRate;

            discCosts.OwnershipCosts.TaxesAndFees   = undiscCosts.OwnershipCosts.TaxesAndFees   * consumerDiscRate;
            discCosts.OwnershipCosts.FinancingCosts = undiscCosts.OwnershipCosts.FinancingCosts * consumerDiscRate;
            discCosts.OwnershipCosts.InsuranceCosts = undiscCosts.OwnershipCosts.InsuranceCosts * consumerDiscRate;

            // energy costs
            discCosts.InitialRetailFuelCosts = undiscCosts.InitialRetailFuelCosts * consumerDiscRate;
            discCosts.RetailFuelCosts        = undiscCosts.RetailFuelCosts        * consumerDiscRate;
            discCosts.ReboundFuelCosts       = undiscCosts.ReboundFuelCosts       * consumerDiscRate;
            discCosts.DriveSurplus           = undiscCosts.DriveSurplus           * consumerDiscRate;
            discCosts.ReallocatedVMTSurplus  = undiscCosts.ReallocatedVMTSurplus  * consumerDiscRate;
            discCosts.RefuelSurplus          = undiscCosts.RefuelSurplus          * consumerDiscRate;
        }

        #endregion

        #endregion

        #region <<< Properties >>>

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

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

        int MinYear => this.ComplianceModel.MinYear;
        //int MaxYear => this.ComplianceModel.MaxYear;

        #endregion

        #region <<< Variables >>>

        //----- constants -----
        const double MetricToShortTon = 1.10231;
        const double PaybackInitValue = 99;

        //----- runtime variables -----
        Dictionary<int, BaselineVehicleEffects[]> _baseVehEff; // indexed by: year, veh
        Dictionary<int, NewSalesSurplusValues> _baseNewSalesSurplusValues; // indexed by: year
        Dictionary<int, RCAltFuelValue[][]> _baseCommercialOppCost; // indexed by: year, age, disc-rate, reg-class, fuel-type

        #endregion

    }
}
