#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

using System.Collections.Generic;
using System.Threading.Tasks;

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

namespace Volpe.Cafe.EffectsModels
{
    /// <summary>
    /// Provides methods for calculating a subset of modeling effects and costs, required for updating the CAFE Model's
    /// user interface elements.
    /// </summary>
    public class EffectsModelLite : IEffectsModel<EffectsDataLite>
    {
        #region <<< Nested Classes >>>

        private class VehicleEffectsData
        {
            public VehicleEffectsData(IEffectsVehicle effectsVeh, bool initState)
            {
                this.EffVehicle = effectsVeh;
                this.InitState = initState;
                this.RCIndex = this.RegClass.GetAltListIndex();
            }

            // vehicle effects
            public double Fleet;
            public double VMT;
            public double Gallons;
            public double BTU;
            public double EnergyUse;

            public UpstreamEmissionsValue Upstream = new UpstreamEmissionsValue();
            public MonetizedEmissionsValue Downstream = new MonetizedEmissionsValue();

            // vehicle costs
            public double RetailFuelCosts;
            public MonetizedEmissionsValue EmissionCosts = new MonetizedEmissionsValue();

            // input vehicle data
            public FuelValue OnRoadFE => this.InitState ? this.EffVehicle.InitOnRoadFE : this.EffVehicle.OnRoadFE;
            public FuelValue FuelShare => this.InitState ? this.EffVehicle.InitFuelShare : this.EffVehicle.FuelShare;

            public double Sales => this.EffVehicle.Sales;

            public int RCIndex { get; }
            public RegulatoryClass RegClass => this.EffVehicle.RegClass;
            public VehicleClass VehClass => this.EffVehicle.VehicleClass;
            public VehicleStyle Style => this.EffVehicle.Style;

            private IEffectsVehicle EffVehicle { get; }
            private bool InitState { get; }

            public void Reset()
            {
                this.Fleet = 0;
                this.VMT = 0;
                this.Gallons = 0;
                this.BTU = 0;
                this.EnergyUse = 0;

                this.Upstream.Clear();
                this.Downstream.Clear();

                this.RetailFuelCosts = 0;
                this.EmissionCosts.Clear();
            }

            public double GetTotalEmissions(Pollutant pollutant)
            {
                return this.Upstream[pollutant].Total + this.Downstream[pollutant];
            }

        }

        #endregion

        #region <<< Constructors >>>

        /// <summary>Initializes a new instance of the <see cref="EffectsModelLite"/> class.</summary>
        /// <param name="complianceModel">
        ///     The compliance model to associate with this <see cref="EffectsModelLite"/> instance.
        /// </param>
        public EffectsModelLite(ICompliance complianceModel)
        {
            this.ComplianceModel = complianceModel;
        }

        #endregion

        #region <<< Variables >>>

        private const double MetricToShortTon = 1.10231;

        #endregion

        #region <<< Properties >>>

        public ICompliance ComplianceModel { get; }
        public ModelingSettings Settings => this.ComplianceModel.Settings;

        private Parameters Parameters => this.Settings.Parameters;
        private RuntimeSettings RuntimeSettings => this.Settings.RuntimeSettings;

        #endregion

        #region <<< Methods >>>

        public EffectsDataLite[] CalculateEffects(Scenario scen)
        {
            int minYear     = this.ComplianceModel.MinYear;
            int maxYear     = this.ComplianceModel.MaxYear;

            var effectsData = new EffectsDataLite[maxYear - minYear + 1];
            var vehs        = this.ComplianceModel.GetData(scen).Vehicles;
            var aggregator  = new VehicleAggregator(vehs, scen, this.Settings);

            Parallel.For(minYear, maxYear + 1, Threading.ParallelOptions,
                year => {
                    var aggregateVehs   = aggregator.Aggregate(year);
                    var effectsDataLite = this.CalculateEffects(scen, year, aggregateVehs);

                    lock (effectsData) {
                        effectsData[maxYear - year] = effectsDataLite;
                    }
                });

            return effectsData;
        }

        public EffectsDataLite CalculateEffects(Scenario scen, int year,
                                                IReadOnlyList<IEffectsVehicle> vehs,
                                                VMTModel vmtModel = null,
                                                ScrappageModel scrappageModel = null)
        {
            vmtModel = vmtModel ?? VMTModel.CreateStaticVMTModel(this.ComplianceModel);
            scrappageModel = scrappageModel ?? ScrappageModel.CreateStaticScrappageModel(this.ComplianceModel);

            VSValue<double[]> survRates = scrappageModel.GetSurvivalRates(year);
            EffectsDataLite effectsData = new EffectsDataLite(year);

            foreach (IEffectsVehicle veh in vehs) {
                if (veh.Sales == 0) {
                    continue;
                }

                var vehDataInit  = new VehicleEffectsData(veh, initState: true);
                var vehDataFinal = new VehicleEffectsData(veh, initState: false);

                double[] survRate = survRates[veh.Style];

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

                    foreach (FuelType fuelType in FuelTypes.List) {
                        double initFS = veh.InitFuelShare[fuelType];
                        double vehFS  = veh.FuelShare[fuelType];

                        // "initial" vehicle effects & costs
                        if (initFS != 0) {
                            vehDataInit.Reset();

                            this.CalculateVehicleEffects(year, age, fuelType, vehDataInit, vmtModel, survRate);
                            this.CalculateVehicleCosts(year, age, fuelType, vehDataInit);
                            this.AppendVehicleEffects(fuelType, vehDataInit, effectsData.InitialState);
                        }

                        // "final" vehicle effects & costs
                        if (vehFS != 0) {
                            vehDataFinal.Reset();

                            this.CalculateVehicleEffects(year, age, fuelType, vehDataFinal, vmtModel, survRate);
                            this.CalculateVehicleCosts(year, age, fuelType, vehDataFinal);
                            this.AppendVehicleEffects(fuelType, vehDataFinal, effectsData.FinalState);
                        }
                    }
                }
            }

            return effectsData;
        }

        //----- per-vehicle effects calcs -----
        private void CalculateVehicleEffects(int year, int age, FuelType fuelType,
                                             VehicleEffectsData vehData, VMTModel vmtModel, double[] survRate)
        {
            this.CalculateEnergyEffects(year, age, fuelType, vehData, vmtModel, survRate);
            this.CalculateEmissionEffects(year, age, fuelType, vehData);
        }

        private void CalculateEnergyEffects(int year, int age, FuelType fuelType,
                                            VehicleEffectsData vehData, VMTModel vmtModel, double[] survRate)
        {
            vehData.Fleet = vehData.Sales * survRate[age] * vehData.FuelShare[fuelType];
            vehData.VMT = vehData.Fleet / 1000
                        * vmtModel.GetVMT(year, age, vehData.Style, vehData.OnRoadFE, vehData.FuelShare);
            vehData.Gallons = vehData.VMT / vehData.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 energyDensity = this.Parameters.FuelProperties.EnergyDensity;
            if (fuelType.IsGEGFuel()) {
                vehData.BTU       = vehData.Gallons * energyDensity.Gasoline;
                vehData.EnergyUse = vehData.BTU / energyDensity[fuelType];
            }
            else {
                vehData.BTU       = vehData.Gallons * energyDensity[fuelType];
                vehData.EnergyUse = vehData.Gallons;
            }
        }

        private void CalculateEmissionEffects(int year, int age, FuelType fuelType, VehicleEffectsData vehData)
        {
            bool isGas = (fuelType == FuelType.Gasoline || fuelType == FuelType.Ethanol85);
            bool isDsl = (fuelType == FuelType.Diesel);

            var upstreamEmissions = this.Parameters.UpstreamEmissions[fuelType, year + age];
            var fuelAssumptions   = this.Parameters.FuelImportAssumptions[fuelType, year + age];

            var    fuelProperties       = this.Parameters.FuelProperties;
            double co2TailpipeRate      = fuelProperties.CO2Emissions[fuelType];
            double so2TailpipeRate      = fuelProperties.SO2Emissions[fuelType];
            var    gasolineTailpipeRate = this.Parameters.TailpipeGasoline[vehData.VehClass, year, age];
            var    dieselTailpipeRate   = this.Parameters.TailpipeDiesel[vehData.VehClass, year, age];

            foreach (var pollutant in Pollutants.MonetizedList) {
                bool isCO2 = (pollutant == Pollutant.CO2);
                bool isGHG = (isCO2 || pollutant == Pollutant.CH4 || pollutant == Pollutant.N2O);
                bool isSO2 = (pollutant == Pollutant.SO2);

                // calculate upstream emissions
                var ueParamsByPollutant = upstreamEmissions[pollutant];
                var upstreamByPollutant = vehData.Upstream[pollutant];

                if (fuelType.IsGEGFuel()) {
                    upstreamByPollutant.Total = ueParamsByPollutant.Total * vehData.BTU / 1e9;
                }
                else {
                    upstreamByPollutant.FuelTSD        = ueParamsByPollutant.FuelTSD        * vehData.BTU / 1e9;
                    upstreamByPollutant.Refining       = ueParamsByPollutant.Refining       * vehData.BTU / 1e9;
                    upstreamByPollutant.Extraction     = ueParamsByPollutant.Extraction     * vehData.BTU / 1e9;
                    upstreamByPollutant.Transportation = ueParamsByPollutant.Transportation * vehData.BTU / 1e9;

                    if (!isGHG) {
                        double shareReducedDomRef = fuelAssumptions.FuelSavingsLeadingToReducedRefining;
                        double shareFromDomCrude  = fuelAssumptions.ReducedRefiningFromDomesticCrude;

                        upstreamByPollutant.Refining       *= shareReducedDomRef;
                        upstreamByPollutant.Extraction     *= shareReducedDomRef * shareFromDomCrude;
                        upstreamByPollutant.Transportation *= shareReducedDomRef * shareFromDomCrude;
                    }

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

                // calculate tailpipe (downstream) emissions
                vehData.Downstream[pollutant] =
                      (isCO2) ? co2TailpipeRate * vehData.EnergyUse / 1e3           // grams/energy-unit * energy-units
                    : (isSO2) ? so2TailpipeRate * vehData.BTU / 1e9                 // grams/MMBTU * BTU
                    : (isGas) ? gasolineTailpipeRate[pollutant] * vehData.VMT / 1e3 // grams/mile * miles
                    : (isDsl) ? dieselTailpipeRate[pollutant] * vehData.VMT / 1e3   // grams/mile * miles
                    : 0;
            }

            // add in brake & tire wear to downstream PM
            var btwEmissions = this.Parameters.BrakeTireWearEmissions[year, vehData.VehClass, fuelType];
            vehData.Downstream[Pollutant.PM] += btwEmissions.Total * vehData.VMT / 1e3;
        }

        //----- per-vehicle costs calcs -----
        private void CalculateVehicleCosts(int year, int age, FuelType fuelType, VehicleEffectsData vehData)
        {
            int calendarYear = year + age;

            this.CalculateRetailFuelCosts(calendarYear, fuelType, vehData);
            this.CalculateCriteriaEmissionCosts(calendarYear, fuelType, vehData);
            this.CalculateGHGEmissionCosts(calendarYear, vehData);
        }

        private void CalculateRetailFuelCosts(int calendarYear, FuelType fuelType, VehicleEffectsData vehData)
        {
            var fuelProperties = this.Parameters.FuelProperties;
            double fuelPriceCY = this.Parameters.FuelPrices.GetFuelPrice(fuelType, calendarYear, fuelProperties);
            double benefitsScale = this.RuntimeSettings.ConsumerBenefitsScale;

            vehData.RetailFuelCosts = vehData.Gallons * fuelPriceCY * benefitsScale;
        }

        private void CalculateCriteriaEmissionCosts(int calendarYear, FuelType fuelType, VehicleEffectsData vehData)
        {
            // criteria costs for upstream emissions
            var criteriaCosts = this.Parameters.CriteriaEmissionCosts.DR3[calendarYear];
            var upstream = vehData.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 == FuelType.Diesel) {
                downstreamSources = vehData.VehClass.IsLDT2b3()
                                  ? criteriaCosts.DownstreamDsl2b3
                                  : criteriaCosts.DownstreamDslLDx;
            }
            else if (fuelType == FuelType.Gasoline || fuelType == FuelType.Ethanol85) {
                downstreamSources = vehData.VehClass.IsLDT2b3() ? criteriaCosts.DownstreamGas2b3
                                  : vehData.VehClass.IsLDT12a() ? criteriaCosts.DownstreamGasLDT
                                  :                               criteriaCosts.DownstreamGasLDV;
            }

            var downstream = vehData.Downstream;
            double downstreamCostsNOX = 0;
            double downstreamCostsSO2 = 0;
            double downstreamCostsPM  = 0;

            if (downstreamSources != null) {
                downstreamCostsNOX = downstream.NOx * downstreamSources.NOX;
                downstreamCostsSO2 = downstream.SO2 * downstreamSources.SO2;
                downstreamCostsPM  = downstream.PM  * downstreamSources.PM;
            }

            // combine upstream and downstream costs
            vehData.EmissionCosts[Pollutant.NOx] = MetricToShortTon * (upstreamCostsNOX + downstreamCostsNOX) / 1000;
            vehData.EmissionCosts[Pollutant.SO2] = MetricToShortTon * (upstreamCostsSO2 + downstreamCostsSO2) / 1000;
            vehData.EmissionCosts[Pollutant.PM]  = MetricToShortTon * (upstreamCostsPM  + downstreamCostsPM)  / 1000;

            // CO and VOC are not considered by EPA health impact analysis.
            vehData.EmissionCosts[Pollutant.CO]  = 0;
            vehData.EmissionCosts[Pollutant.VOC] = 0;
        }

        private void CalculateGHGEmissionCosts(int calendarYear, VehicleEffectsData vehData)
        {
            var ghgCosts = this.Parameters.GreenhouseEmissionCosts;
            double co2Costs = ghgCosts.GetCO2Costs(Estimates.Average, calendarYear);
            double ch4Costs = ghgCosts.GetCH4Costs(Estimates.Average, calendarYear);
            double n2oCosts = ghgCosts.GetN2OCosts(Estimates.Average, calendarYear);

            vehData.EmissionCosts[Pollutant.CO2] = vehData.GetTotalEmissions(Pollutant.CO2) * co2Costs / 1000;
            vehData.EmissionCosts[Pollutant.CH4] = vehData.GetTotalEmissions(Pollutant.CH4) * ch4Costs / 1000;
            vehData.EmissionCosts[Pollutant.N2O] = vehData.GetTotalEmissions(Pollutant.N2O) * n2oCosts / 1000;
        }

        //----- effects & costs aggregation -----
        private void AppendVehicleEffects(FuelType fuelType,
                                          VehicleEffectsData vehData,
                                          EffectsDataLite.DataItem effectsData)
        {
            int rcIndex = vehData.RCIndex;

            effectsData.Fleet          .Items[rcIndex].Append(fuelType, vehData.Fleet);
            effectsData.VMT            .Items[rcIndex].Append(fuelType, vehData.VMT);
            effectsData.Gallons        .Items[rcIndex].Append(fuelType, vehData.Gallons);
            effectsData.RetailFuelCosts.Items[rcIndex].Append(fuelType, vehData.RetailFuelCosts);

            var emEffs  = effectsData.Emissions;
            var emCosts = effectsData.EmissionCosts;

            foreach (Pollutant pollutant in Pollutants.MonetizedList) {
                emEffs[pollutant] .Items[rcIndex].Append(fuelType, vehData.GetTotalEmissions(pollutant));
                emCosts[pollutant].Items[rcIndex].Append(fuelType, vehData.EmissionCosts[pollutant]);
            }
        }

        #endregion
    }
}
