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

using Volpe.Cafe.Generic;
using Volpe.Cafe.Logging;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.TechAnalysis;
using Volpe.Cafe.VehicleSimulation;

using TechKey = Volpe.Cafe.TechKeys.TechKey128;
using TA = Volpe.Cafe.Model.TechnologyApplicability;
using TI = Volpe.Cafe.TechnologyIndexes;
#endregion

namespace Volpe.Cafe.Model
{
    /// <summary>
    /// Provides a compliance solution for applying one or more technologies to a vehicle or a group of vehicles.
    /// <br/><br/>
    /// This class contains an <see cref="ExamineFinding"/> method, which is used to examine a finding based on a single
    /// vehicles/technologies combination.
    /// </summary>
    [Serializable]
    public class ComplianceFinding
    {

        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="ComplianceFinding"/> class.
        /// </summary>
        /// <param name="compliance">The compliance model that is the parent of this compliance finder.</param>
        public ComplianceFinding(ICompliance compliance)
        {
            this._compliance = compliance;
            this._component = null;
            this._affectedVehs = new List<AffectedVehicle>();
            this.AffectedRC = new RCValue<bool>();
            this.TechSolutions = new TechSolution[8]; // pre-allocate space for solutions
            this.BindingProgram = null;
            this.EfficiencyCAFE = double.NaN;
            this.EfficiencyCO2 = double.NaN;
            this.Efficiency = double.NaN;
        }

        #endregion

        #region <<< Methods >>>

        /// <summary>
        /// Resets the runtime variables of this compliance finding for a new test.
        /// </summary>
        public void Reset()
        {
            this._version  = 0;
            this._component = null;
            this._affectedVehs.Clear();

            this.AffectedRC.Clear();
            this.HasVehs = false;
            this.BindingProgram = null;
            this.EfficiencyCAFE = double.NaN;
            this.EfficiencyCO2 = double.NaN;
            this.Efficiency = double.NaN;
            this.IsApplied = false;
        }

        /// <summary>
        /// Examines the effect of applying the specified technology on a given <see cref="IOEMProduct"/>.
        /// </summary>
        /// <param name="scen">The scenario being evaluated by the model.</param>
        /// <param name="iteration">The convergence iteration being evaluated by the model.</param>
        /// <param name="year">The model year being evaluated by the model.</param>
        /// <param name="mfr">The manufacturer being evaluated by the model.</param>
        /// <param name="tech">The technology to evaluate.</param>
        /// <param name="product">A <see cref="IOEMProduct"/> to evaluate.</param>
        /// <param name="cfCount">A reference to the total count of compliance findings generated thus far for the
        ///     current technology. When this method returns, if a new compliance finding was generated, this value
        ///     will be incremented.</param>
        public void ExamineFinding(
            Scenario scen, int iteration, ModelYear year, IManufacturer mfr, TechnologyInfo tech, IOEMProduct product,
            ref int cfCount)
        {
            this.ExamineCommon(scen, iteration, year, mfr, tech, product);
            if (this.HasVehs) {
                this._version = cfCount++;
            }
        }

        /// <summary>
        /// Examines the effect of inheriting the specified technology on a given vehicle.
        /// </summary>
        /// <param name="scen">The scenario being evaluated by the model.</param>
        /// <param name="iteration">The iteration being evaluated by the model.</param>
        /// <param name="year">The model year being evaluated by the model.</param>
        /// <param name="mfr">The manufacturer being evaluated by the model.</param>
        /// <param name="tech">The technology to evaluate.</param>
        /// <param name="vehicle">A vehicle to evaluate.</param>
        public void ExamineTechInheriting(Scenario scen, int iteration, ModelYear year, IManufacturer mfr,
            TechnologyInfo tech, IVehicle vehicle)
        {
            this.ExamineCommon(scen, iteration, year, mfr, tech, vehicle, doInherit: true);
        }

        /// <summary>
        /// Examines the effect of forcing the application of the specified technology on a given vehicle.
        /// </summary>
        /// <param name="scen">The scenario being evaluated by the model.</param>
        /// <param name="iteration">The iteration being evaluated by the model.</param>
        /// <param name="year">The model year being evaluated by the model.</param>
        /// <param name="mfr">The manufacturer being evaluated by the model.</param>
        /// <param name="tech">The technology to evaluate.</param>
        /// <param name="vehicle">A vehicle to evaluate.</param>
        public void ExamineForcedApplication(Scenario scen, int iteration, ModelYear year, IManufacturer mfr,
            TechnologyInfo tech, IVehicle vehicle)
        {
            this.ExamineCommon(scen, iteration, year, mfr, tech, vehicle, forcedTech: true);
        }

        private void ExamineCommon(
            Scenario scen, int iteration, int year, IManufacturer mfr, TechnologyInfo tech, IOEMProduct product,
            bool doInherit = false, bool forcedTech = false)
        {
            this.Refresh(scen, iteration, year, mfr, tech, doInherit, forcedTech);
            this.ParseTechnologies(product);
            if (this.HasVehs) {
                this.CalcEfficiency();
            }
        }

        /// <summary>
        /// Refreshes the input variables and resets some of the runtime variables of this CF for a new test.
        /// </summary>
        private void Refresh(
            Scenario scen, int iteration, int year, IManufacturer mfr, TechnologyInfo tech,
            bool doInherit = false, bool forcedTech = false)
        {
            this._scen = scen;
            this._iter = iteration;
            this._year = year;
            this._mfr = mfr;
            this._tech = tech;

            this._doInherit = doInherit;
            this._forcedTech = forcedTech;

            this.Reset();
        }

        /// <summary>
        /// Parses pending technologies for application on a specific product.
        /// </summary>
        private void ParseTechnologies(IOEMProduct product)
        {
            bool isTechValid = false;
            var windowOfApplication = WindowOfApplication.Empty;
            IEnumerable<AffectedVehicle> affectedVehicles = null;

            if (product is IVehicle veh) {
                AffectedVehicle affectedVehicle;
                if (this._doInherit) {
                    isTechValid = true;
                    windowOfApplication = new WindowOfApplication(this._year, this._year);
                    affectedVehicle = new AffectedVehicle(veh, windowOfApplication.MinYear);
                }
                else {
                    isTechValid = TechEvaluator.IsTechValid(
                        this._tech.Index, veh, this._scen, this._year, this.Settings,
                        out windowOfApplication, out affectedVehicle);
                }
                affectedVehicles = new AffectedVehicle[] { affectedVehicle };
            }
            else if (product is IOEMComponent component) {
                isTechValid = TechEvaluator.IsTechValid(
                    this._tech.Index, component, this._scen, this._year, this.Settings,
                    out windowOfApplication, out affectedVehicles);
                this._component = component;
            }

            // forced-techs can only be applied during the analysis year
            if (isTechValid && this._forcedTech && windowOfApplication.IsMultiYear) {
                isTechValid = false;
            }
            if (!isTechValid) {
                return;
            }

            foreach (var affectedVeh in affectedVehicles) {
                var vehRevision = affectedVeh.Vehicle.GetRevision(this._year);
                TechKey techKey = KeyInitializer.TestTechKey(vehRevision, this._tech.Index);

                if (!SimulationDatabase.HasValue(techKey)) {
                    this.LogMissingTechKeyWarning(techKey, vehRevision.Code);
                    continue;
                }

                this._affectedVehs.Add(affectedVeh);

                RegulatoryClass regClass = vehRevision.RegClass;
                if (regClass != RegulatoryClass.None) {
                    if (vehRevision.Sales != 0 || this._forcedTech || this._doInherit) {
                        this.AffectedRC[regClass] = true;
                    }
                    this.HasVehs = true;
                }
            }

            if (this.HasVehs) {
                this.StartYear = windowOfApplication.MinYear;
                this.EndYear = windowOfApplication.MaxYear;
                this.YearCount = this.EndYear - this.StartYear + 1;
            }
        }

        private void LogMissingTechKeyWarning(TechKey techKey, int vehCode)
        {
            this._compliance.LogWriter.SafeLogWarning(
                "The requested technology combination was not found in the simulation database:  " +
                TI.KeyToString(techKey, ';') +
                $" (scen={this._scen.Index}; year={this._year}; veh={vehCode}; tech={this._tech.Name})");
        }

        #region < Calculating Efficiency >

        /// <summary>
        /// Calculates the efficiency rating of this <see cref="ComplianceFinding"/> instance.
        /// </summary>
        void CalcEfficiency()
        {
            this.CheckComplianceState();
            if (this.IsOvercomplying && this.IsMultiYear) {
                // skip over-complying solution during multi-year -- 
                // this should slightly reduce the degree of excessive over-compliance
                return;
            }

            //------------------------------------------------------------------------------------
            // When calculating efficiency of this compliance finding, begin by gathering vehicle
            // and technology impacts, and calculating compliance metrics.
            //                                                                                  
            // Then, from the compliance statistics, obtain changes in CAFE fines, CAFE credit
            // value, and CO2 credit value.
            //------------------------------------------------------------------------------------
            this.InitTechSolutions(); // initialize tech solutions array for all available years
            this.CalcTechImpacts();   // calculate technology impacts for each affected vehicle

            // calc current and new compliance stats for the **proper** analysis year only
            var settings = this.Settings;
            var mfrStats = this._mfr.RevisionInfo.ComplianceStats;
            var tsProper = this.TechSolutions[this._year - this.StartYear];
            tsProper.CurrentStats = Standards.TestCompliance(this._scen, this._year, this._mfr, null    , settings, mfrStats);
            tsProper.NewStats     = Standards.TestCompliance(this._scen, this._year, this._mfr, tsProper, settings, mfrStats);

            //----->
            // calculate deltas from compliance stats:
            //  - calculate changes in CAFE credits, bounded credit valuation, full credit
            //    valuation, and fines
            //  - calculate changes in CO-2 credits, full credit valuation, and bounded credit
            //    valuation
            //  - in both cases, applying discounting to "dollar amounts" as necessary
            //----->
            var curStats = tsProper.CurrentStats;
            var newStats = tsProper.NewStats;
            //  > CAFE
            this._deltaCAFECredits = this._deltaCAFECreditsKGal = 0;
            this._deltaCAFEFines = this._deltaCAFECrValueBound = this._deltaCAFECreditValue = 0;
            foreach (var rc in RegClasses.List)
            {
                this._deltaCAFECredits      += (newStats.Credits    [rc] - curStats.Credits    [rc]);
                this._deltaCAFECreditsKGal  += (newStats.CreditsKGal[rc] - curStats.CreditsKGal[rc]);
                this._deltaCAFECreditValue  += (newStats.CreditValue[rc] - curStats.CreditValue[rc]);
                this._deltaCAFECrValueBound += (Math.Min(0, newStats.CreditValue[rc]) - Math.Min(0, curStats.CreditValue[rc]));
                this._deltaCAFEFines        += (curStats.Fines[rc] - newStats.Fines[rc]);
            }
            //  > CO2
            this._deltaCO2Credits = this._deltaCO2CrValueBound = this._deltaCO2CreditValue = 0;
            foreach (var rc in RegClasses.AltList)
            {
                this._deltaCO2Credits      += (newStats.CO2Credits    [rc] - curStats.CO2Credits    [rc]);
                this._deltaCO2CreditValue  += (newStats.CO2CreditValue[rc] - curStats.CO2CreditValue[rc]);
                this._deltaCO2CrValueBound += (Math.Min(0, newStats.CO2CreditValue[rc]) - Math.Min(0, curStats.CO2CreditValue[rc]));
            }

            //------------------------------------------------------------------------------------
            // Once the technology solutions for all affected model years were calculated,
            // aggregate the total sales, costs, and fuel savings across all years.
            //------------------------------------------------------------------------------------
            this._totalAffectedSales = 0;
            this._totalTechCost = 0;
            this._totalTaxCredit = 0;
            this._totalBatteryTaxCredit = 0;
            this._totalFuelSavings = 0;

            // examine the range of model years during which any vehicles were selected
            for (int i = 0; i < this.YearCount; i++)
            {
                // get technology solution info for the current model year
                var techSolution = this.TechSolutions[i];
                var testVehs     = techSolution.TestVehicles;
                var impacts      = techSolution.TechImpacts;
                var year         = techSolution.ModelYear;

                var mfrRevision = this._mfr.GetRevision(year);

                // iterate each vehicle and tech impacts for the current year
                for (int j = 0; j < testVehs.Count; j++)
                {
                    var    testVeh  = testVehs[j];
                    var    prevVeh  = testVeh.PreviousState;
                    var    vehStyle = testVeh.Style;
                    double vehSales = testVeh.Sales;

                    // compute fuel cost
                    FuelValue gap = settings.Parameters.EconomicValues.Parameters[vehStyle].OnRoadGap;
                    FuelValue kmy = mfrRevision.PseudoFuelCost[vehStyle];
                    double prevFuelCost = prevVeh.GetVehicleFuelCost(gap, kmy);
                    double testFuelCost = testVeh.GetVehicleFuelCost(gap, kmy);

                    // compute tax credits
                    var scenInfo = this._scen[testVeh.RegClass, year];
                    var vehRevision = testVeh.Vehicle.GetRevision(year);
                    int prevHevTech = TA.GetMaxHEV(prevVeh.TechKey);
                    int testHevTech = TA.GetMaxHEV(testVeh.TechKey);

                    double prevTaxCredit = 0;
                    double testTaxCredit = 0;
                    if (scenInfo.ApplyTaxCredits) {
                        double prevMsrp = vehRevision.BaselineInfo.Msrp + vehRevision.ComplianceCosts.TotalTechCost;
                        double testMsrp = prevMsrp + impacts[j].TechCost;
                        prevTaxCredit = TA.GetConstrainedTaxCredit(this._scen, year, prevHevTech, vehRevision, prevMsrp, false);
                        testTaxCredit = TA.GetConstrainedTaxCredit(this._scen, year, testHevTech, vehRevision, testMsrp, false);
                    }

                    double prevBatteryTaxCredit = 0;
                    double testBatteryTaxCredit = 0;
                    if (scenInfo.ApplyBatteryTaxCredits) {
                        prevBatteryTaxCredit = (prevHevTech == -1) ? 0 : scenInfo.GetScaledBatteryTaxCredit(prevHevTech, false);
                        testBatteryTaxCredit = (testHevTech == -1) ? 0 : scenInfo.GetScaledBatteryTaxCredit(testHevTech, false);
                    }

                    // aggregate values ...
                    this._totalAffectedSales    += vehSales;
                    this._totalTechCost         += vehSales * impacts[j].TechCost;
                    this._totalTaxCredit        += vehSales * (testTaxCredit - prevTaxCredit);
                    this._totalBatteryTaxCredit += vehSales * (testBatteryTaxCredit - prevBatteryTaxCredit);
                    this._totalFuelSavings      += vehSales * (prevFuelCost - testFuelCost);
                } // next j (veh/tech)
            } // next i (year)

            //----- calculate efficiency -----
            this.EfficiencyCAFE = 0;
            this.EfficiencyCO2 = 0;

            this.CalcEfficiencyCostPerCredit();

            // update binding program depending on CAFE efficiency and mfr's fine preference
            // we want to treat fine payers as "in compliance" if eff-CAFE is not cost effective
            if (this.PreferCafeFines && this.EfficiencyCAFE >= 0 && this.BindingProgram != null) {
                var bindingProgram = this.BindingProgram.Value;
                if (bindingProgram.IsCAFEAndCO2()) {
                    this.BindingProgram = ComplianceProgram.CO2;
                }
                else if (bindingProgram.IsCAFE()) {
                    this.BindingProgram = null;
                }
            }

            // calc overall efficiency based on CAFE-eff and binding program
            this.Efficiency = this.CalcOverallEfficiency();

            if (this.CheckIgnoredRequired()) {
                this.IgnoreTechnologies();
                this.Efficiency = double.NaN;
            }
        }

        void CalcEfficiencyCostPerCredit()
        {
            //** cost-per-credit eff-cost mode is specified in dollar/credit **

            bool enforceCAFE = this.RuntimeSettings.ComplianceProgram.HasCAFE();
            double deltaComplianceCredits = (enforceCAFE)
                ? this._deltaCAFECreditsKGal
                : this._deltaCO2Credits;

            if ((this._totalAffectedSales <= 0 || deltaComplianceCredits == 0)
                && (this._doInherit || this._forcedTech)) {
                return;
            }

            double techCostsLessTaxCreditsAndFuelSavings =
                this._totalTechCost - this._totalTaxCredit - this._totalBatteryTaxCredit - this._totalFuelSavings;

            this.EfficiencyCAFE = (techCostsLessTaxCreditsAndFuelSavings - this._deltaCAFEFines)
                                / this._deltaCAFECreditsKGal;
            this.EfficiencyCO2 = techCostsLessTaxCreditsAndFuelSavings / this._deltaCO2Credits;
        }

        double CalcOverallEfficiency()
        {
            var complianceProgram = this.RuntimeSettings.ComplianceProgram;
            if (complianceProgram.IsCO2()) {
                return this.EfficiencyCO2;
            }
            if (this.EfficiencyCAFE < 0) {
                return this.EfficiencyCAFE;
            }

            // for "n/a" binding program, assume CAFE eff
            var bindingProgram = this.BindingProgram ?? ComplianceProgram.CAFE;
            return bindingProgram.IsCO2() ? this.EfficiencyCO2 : this.EfficiencyCAFE;
        }

        void CheckComplianceState()
        {
            this.IsInComplianceCAFE = true;
            this.IsInComplianceCO2 = true;
            this.IsOvercomplyingCAFE = true;
            this.IsOvercomplyingCO2 = true;

            var program = this.RuntimeSettings.ComplianceProgram;
            var mfrStats = this._mfr.ComplianceStats;

            if (program.HasCAFE()) {
                // for CAFE, eval each affected RC
                var validRegClasses = RegClasses.List.Where(regClass => this.AffectedRC[regClass]);
                foreach (var regClass in validRegClasses) {
                    this.IsInComplianceCAFE &= this._mfr.GetNetCredits(regClass) >= 0;
                    this.IsOvercomplyingCAFE &= mfrStats.Credits[regClass] >= 0;
                }
            }

            if (program.HasCO2()) {
                // for CO-2, consider total in all RC's
                // (this also allows the model to increase one RC above its
                //  standard to offset a shortfall in another RC)
                this.IsInComplianceCO2 &= this._mfr.GetNetCO2Credits().Total >= 0;
                this.IsOvercomplyingCO2 &= mfrStats.CO2Credits.Total >= 0;
            }

            this.BindingProgram = CalcBindingProgram();
            this.PreferCafeFines = CalcFinePreference();

            // local helper functions
            bool CalcFinePreference()
            {
                bool preferCafeFines = true;
                if (preferCafeFines && program.HasCAFE()) {
                    for (int i = 0; i < this.AffectedRC.Items.Length; i++) {
                        if (this.AffectedRC.Items[i]) {
                            preferCafeFines &= this._mfr.BaselineInfo.PreferFines[RegClasses.List[i], this._year];
                        }
                    }
                }
                return preferCafeFines;
            }

            ComplianceProgram? CalcBindingProgram()
            {
                if (!program.IsCAFEAndCO2()) {
                    return program;
                }

                // note -- below is not a typo:
                // - if CAFE is in compliance, assume CO2 binding program, and vice-versa
                // - if neither is in compliance, assume both are binding
                // - do not negate (that is, !this.IsInComplianceCAFE), as that doesn't work
                return this.IsInCompliance ? (ComplianceProgram?)null
                     : this.IsInComplianceCAFE ? ComplianceProgram.CO2
                     : this.IsInComplianceCO2 ? ComplianceProgram.CAFE
                     : ComplianceProgram.CAFEAndCO2;
            }
        }

        /// <summary>
        /// Returns: <c>true</c>, if we should ignore this technology solution; <c>false</c>, if we should consider it.
        /// </summary>
        bool CheckIgnoredRequired()
        {
            //------------------------------------------------------------------------------------
            // We want to ignore technology solutions under some circumstances:
            //  - Ignore solutions that adversely affect compliance state. For multiyear
            //    solutions, also check CAFE and CO2 ratings in preceding years and do not allow
            //    either to decrease, since generated credits from earlier years may have already
            //    been used.
            //  - Ignore solutions that reduce fuel savings, but only when there is no change in
            //    compliance state.
            //  - For inefficient solutions only, ignore technologies if there is no impact on the
            //    binding compliance program (whether positive or negative).
            //
            // Exclusions:
            //  - For technologies that are being inherited, we still want to apply those
            //    technologies in order to be consistent with platforming rules.
            //  - For technologies designated as "forced" (if any exist), those serve as
            //    prerequisites and must be applied on a vehicle prior to evaluating any other
            //    technologies.
            //------------------------------------------------------------------------------------

            if (this._doInherit || this._forcedTech) {
                return false;
            }

            var cp = this.RuntimeSettings.ComplianceProgram;
            bool hasCAFE = cp.HasCAFE();
            bool hasCO2 = cp.HasCO2();

            if ((hasCAFE && this._deltaCAFECredits < 0) || (hasCO2 && this._deltaCO2Credits < 0)) {
                return true;
            }

            if (this._totalFuelSavings < 0 && ((hasCAFE && this._deltaCAFECredits == 0)
                                             || (hasCO2 && this._deltaCO2Credits == 0))) {
                return true;
            }

            if (!this.IsEfficient && this.BindingProgram != null) {
                switch (this.BindingProgram.Value) {
                    case ComplianceProgram.CO2:
                        if (this._deltaCO2Credits == 0) {
                            return true;
                        }
                        break;

                    case ComplianceProgram.CAFE:
                        if (this._deltaCAFECredits == 0) {
                            return true;
                        }
                        break;

                    case ComplianceProgram.CAFEAndCO2:
                        if (this._deltaCO2Credits == 0 && this._deltaCAFECredits == 0) {
                            return true;
                        }
                        break;
                }
            }

            if (this.IsMultiYear) {
                // this check is computationally most expensive, so leave it for the end
                bool hasComplianceDecreased = Standards.HasComplianceDecreased(
                    this._scen, this.StartYear, this.EndYear, this._mfr, this.TechSolutions, this.Settings, cp);

                if (hasComplianceDecreased) {
                    return true;
                }
            }

            return false;
        }

        void InitTechSolutions()
        {
            // resize array as needed
            if (this.TechSolutions.Length < this.YearCount) {
                int newCapacity = (this.YearCount / 8 + 1) * 8;
                TechSolution[] newTechSolutions = new TechSolution[newCapacity];
                Array.Copy(this.TechSolutions, newTechSolutions, this.TechSolutions.Length);
                this.TechSolutions = newTechSolutions;
            }

            // init tech solutions for each selected year
            for (int i = 0; i < this.YearCount; i++) {
                int year = this.StartYear + i;
                if (this.TechSolutions[i] == null) {
                    this.TechSolutions[i] = new TechSolution(year);
                }
                else {
                    this.TechSolutions[i].Refresh(year);
                }
            }
        }

        /// <summary>
        /// Sets the "IsIgnored" state to true for a candidate technology on the affected component and vehicle(s)
        /// associated with this <see cref="ComplianceFinding"/> instance.
        /// </summary>
        /// <remarks>
        /// Once the "IsIgnored" state is set on the component/vehicle(s) for a given technology, that technology will
        /// not be available for application until it is cleared. Typically the compliance model clears the "IsIgnored"
        /// state for all components and vehicles at the beginning of the model year loop.
        /// </remarks>
        public void IgnoreTechnologies()
        {
            if (this.IsComponentLevel) {
                this._component.TechUtilization[this._tech.Index].IsIgnored = true;
            }
            foreach (var veh in this._affectedVehs) {
                veh.Vehicle.TechUtilization[this._tech.Index].IsIgnored = true;
            }
        }

        /// <summary>
        /// Calculates the impacts of pending technologies on each selected vehicle model within the selected range of
        /// model years.
        /// </summary>
        void CalcTechImpacts()
        {
            //
            // TODO: Update CalcTechImpacts to use pre-allocated variables
            //  - For example, declare affectedVehIndex, then use it as:
            //      techSolution.ApplicationYears[affectedVehIndex] = vehInitialApplicationYear;
            //      TechImpact impact = techSolution.TechImpacts[affectedVehIndex];
            //      TestVehicle testVeh = techSolution.TestVehicles[affectedVehIndex];
            //


            // examine the range of model years during which any vehicles were selected
            for (int i = 0; i < this.YearCount; i++) {
                var techSolution = this.TechSolutions[i];
                int year = techSolution.ModelYear;

                // evaluate each selected vehicle
                foreach (var affectedVeh in this._affectedVehs) {
                    if (affectedVeh.ApplicationYear > year) {
                        continue;
                    }

                    var impact = this.CalcVehicleTechImpact(year, affectedVeh.Vehicle);
                    var testVeh = this.CalcTestVehicle(year, affectedVeh.Vehicle, impact);

                    techSolution.Update(affectedVeh.ApplicationYear, impact, testVeh);
                }
            }
        }
        TechImpact CalcVehicleTechImpact(int year, IVehicle veh)
        {
            // calculate effect (cost, improvement, etc.) of pending technology
            TechEffect techEffect = TA.GetTechEffect(this._scen, year, veh, this._tech, this.Settings);

            // translate tech-effect into new tech-impact for the vehicle
            TechImpact impact = new TechImpact(this._tech) { };

            // affected technology info
            impact.TechKey         = techEffect.KeyNew;
            // fuel consumption improvements
            impact.FE1ImprvRef     = techEffect.FE1ImprvRef;
            impact.FE2ImprvRef     = techEffect.FE2ImprvRef;
            impact.FE1ImprvNew     = techEffect.FE1ImprvNew;
            impact.FE2ImprvNew     = techEffect.FE2ImprvNew;
            impact.FE1Adjustment   = techEffect.FE1ImprvRef / techEffect.FE1ImprvNew;
            impact.FE2Adjustment   = techEffect.FE2ImprvRef / techEffect.FE2ImprvNew;
            impact.FS2             = techEffect.FS2;
            // costs
            impact.BatteryCostRef  = techEffect.BatteryCostRef;
            impact.BatteryCostNew  = techEffect.BatteryCostNew;
            impact.BatteryCost     = techEffect.BatteryCostNew - techEffect.BatteryCostRef;
            impact.TechCost        = techEffect.NonBatteryCost + impact.BatteryCost;
            impact.MaintRepairCost = techEffect.MaintRepairCost;
            // credits
            impact.ZEVCredit       = techEffect.ZEVCredit;
            // weight changes
            impact.DeltaCW         = techEffect.DeltaCW;
            impact.DeltaGVWR       = techEffect.DeltaGVWR;
            impact.DeltaGCWR       = techEffect.DeltaGCWR;

            return impact;
        }
        TestVehicle CalcTestVehicle(int year, IVehicle veh, TechImpact impact)
        {
            TestVehicle testVeh = new TestVehicle(year, veh); // init new test vehicle wrapper

            //-------------------------------------------------------------------------->
            // Examine certain technologies with special considerations:
            //  1)  detect if the technology triggers a fuel type change,
            //  2a) if there is no fuel type change, continue as usual,
            //  2b) if there is a fuel type change, migrate fuel share and fuel economy
            //      values to the new fuel type.
            //-------------------------------------------------------------------------->
            FuelValue vehFE  = testVeh.FuelEconomy;
            FuelValue vehFS  = testVeh.FuelShare;
            HEVType   vehHEV = testVeh.HevType;
            FuelType  vehFT  = vehFS.FuelType;
            FuelType  vehPrimaryFuel = vehFS.PrimaryFuel;
            int       techIndex = this._tech.Index;

            if (TI.IsConversionToDiesel(techIndex) && (vehFT & FuelType.Diesel) != FuelType.Diesel)
            {
                this.ConvertFuel(vehPrimaryFuel, FuelType.Diesel, ref vehFE, ref vehFS);
            }
            else if (TI.IsConversionToCNG(techIndex) && (vehFT & FuelType.CNG) != FuelType.CNG)
            {
                this.ConvertFuel(vehPrimaryFuel, FuelType.CNG, ref vehFE, ref vehFS);
            }
            else if (TI.IsConversionToSHEV(techIndex) && vehHEV != HEVType.StrongHybrid)
            {
                // NOTES:
                //  - all of our simulated SHEVs are gasoline
                //  - however, we allow diesels to convert to SHEVs, but to gas SHEVs only
                this.ConvertFuel(vehPrimaryFuel, FuelType.Gasoline, ref vehFE, ref vehFS);
                vehHEV = HEVType.StrongHybrid; // update veh HEV type to reflect conversion to new tech
            }
            else if (TI.IsConversionToPHEV(techIndex))
            {
                //--------------------------------------------------------------------------------
                // When initially converting a vehicle to a PHEV, use the vehicle's primary FE
                // (usually gasoline, though could also be diesel) as the starting point for both
                // the primary and secondary FE values of the resulting plug-in hybrid/electric
                // vehicle (PHEV). This is required because the vehicle simulation results are
                // defined using the same assumptions. However, set the primary fuel type as
                // gasoline and secondary as electricity. This is because all of our simulated
                // PHEVs are gas-electric.
                //
                // If a vehicle is already a PHEV, no fuel conversion is necessary. Only the fuel
                // shares need to be update to new values.
                //--------------------------------------------------------------------------------
                if (vehHEV != HEVType.PlugInHybrid)
                {
                    double vehPrimaryFE = vehFE[vehPrimaryFuel];
                    vehFE.Clear();
                    vehFS.Clear();
                    vehFE[FuelType.Gasoline   ] = vehPrimaryFE;
                    vehFE[FuelType.Electricity] = vehPrimaryFE;
                }
                // update fuel shares and HEV type to reflect conversion to new tech
                vehFS[FuelType.Gasoline   ]  = 1 - impact.FS2;
                vehFS[FuelType.Electricity]  = impact.FS2;
                vehHEV = HEVType.PlugInHybrid;
            }
            else if (TI.IsConversionToEV(techIndex) && vehHEV != HEVType.BatteryElectric)
            {
                this.ConvertFuel(vehPrimaryFuel, FuelType.Electricity, ref vehFE, ref vehFS);
                vehHEV = HEVType.BatteryElectric; // update veh HEV type to reflect conversion to new tech
            }
            else if (TI.IsConversionToFCV(techIndex) && vehHEV != HEVType.FuelCell)
            {
                this.ConvertFuel(vehPrimaryFuel, FuelType.Hydrogen, ref vehFE, ref vehFS);
                vehHEV = HEVType.FuelCell; // update veh HEV type to reflect conversion to new tech
            }

            //-------------------------------------------------------------------------->
            // After fuel conversion and FC improvement factors were applied, apply FE
            // adjustment factors.
            //-------------------------------------------------------------------------->
            vehFE[vehFS.PrimaryFuel] *= impact.FE1Adjustment;
            if (vehFS.IsMultiFuel)
            {   // additional logic for multi-fuel vehicles --
                //  - for E85 :  use primary FC-adj
                //  - for Elec:  use secondary FC-adj
                if (vehFS[FuelType.Ethanol85  ] > 0) { vehFE[FuelType.Ethanol85  ] *= impact.FE1Adjustment; }
                if (vehFS[FuelType.Electricity] > 0) { vehFE[FuelType.Electricity] *= impact.FE2Adjustment; }
            }

            // update test vehicle fuel economy info and attributes
            testVeh.FuelEconomy = vehFE;
            testVeh.FuelShare   = vehFS;
            testVeh.HevType     = vehHEV;
            testVeh.CurbWeight -= impact.DeltaCW;
            testVeh.GVWR       -= impact.DeltaGVWR;
            testVeh.GCWR       -= impact.DeltaGCWR;
            testVeh.TechKey     = impact.TechKey;

            return testVeh;
        }
        void ConvertFuel(FuelType fromFuel, FuelType toFuel, ref FuelValue vehFE, ref FuelValue vehFS)
        {
            double newFE = vehFE[fromFuel];
            vehFE.Clear();
            vehFS.Clear();
            vehFE[toFuel] = newFE;
            vehFS[toFuel] = 1;
        }

        #endregion

        #region < Applying CF >

        /// <summary>
        /// Applies the pending technologies to all affected vehicles, across all affected model years, that are
        /// represented within this compliance finding.
        /// </summary>
        public void ApplyFinding()
        {
            if (!this.IsValid) {
                throw new InvalidOperationException("The ComplianceFinding being applied is not valid.");
            }
            if (this.IsApplied) {
                throw new InvalidOperationException("The ComplianceFinding has already been applied.");
            }

            // mark finding as applied and log it
            this.IsApplied = true;
            this.LogFinding();

            // apply technology on a component
            if (this.IsComponentLevel) {
                this._component.InstallTechnology(
                    this._scen, this._year, this.StartYear, this._tech, false, this.Settings);
            }

            // examine the range of model years during which any vehicles were selected
            for (int i = 0; i < this.YearCount; i++) {
                var techSolution = this.TechSolutions[i];
                var vehYears     = techSolution.ApplicationYears;
                var impacts      = techSolution.TechImpacts;
                var testVehs     = techSolution.TestVehicles;
                int year         = techSolution.ModelYear;

                for (int j = 0; j < testVehs.Count; j++) {
                    this.ApplyVehTechImpact(year, vehYears[j], impacts[j], testVehs[j]);
                }
            }
        }
        void ApplyVehTechImpact(int complianceYear, int applicationYear, TechImpact impact, TestVehicle testVeh)
        {
            if (!testVeh.FuelEconomy.IsValid || !testVeh.FuelShare.IsValid) {
                throw new InvalidOperationException(
                    "Error in ComplianceFinding. Vehicle's new Fuel Economy and/or Fuel Share values are not valid.");
            }

            var tech = impact.Technology;
            var veh = testVeh.Vehicle;
            var vehRevision = veh.GetRevision(complianceYear);
            var mfrRevision = veh.Manufacturer.GetRevision(complianceYear);

            if (complianceYear == applicationYear) {
                // update vehicle configuration based on the current technology
                bool isInherited = (this._doInherit || applicationYear != this.StartYear);
                veh.InstallTechnology(this._scen, this._year, applicationYear, tech, isInherited, this.Settings);
            }

            // update vehicle fuel economy and fuel share
            vehRevision.FuelEconomy = testVeh.FuelEconomy;
            vehRevision.FuelShare   = testVeh.FuelShare;

            // update vehicle attributes (footprint, CW, etc.)
            vehRevision.Footprint  = testVeh.Footprint;
            vehRevision.CurbWeight = testVeh.CurbWeight;
            vehRevision.GVWR       = testVeh.GVWR;
            vehRevision.GCWR       = testVeh.GCWR;

            // update cost metrics
            //  > veh-level
            vehRevision.ComplianceCosts.BatteryTechCost += impact.BatteryCost;
            vehRevision.ComplianceCosts.NonBatteryTechCost += impact.TechCost - impact.BatteryCost;
            vehRevision.ComplianceCosts.MaintRepairCost += impact.MaintRepairCost;
            //  > mfr-level
            var mfrCosts = mfrRevision.ComplianceCosts[vehRevision.RegClass];
            mfrCosts.BatteryTechCost += (vehRevision.Sales * impact.BatteryCost);
            mfrCosts.NonBatteryTechCost += (vehRevision.Sales * (impact.TechCost - impact.BatteryCost));
            mfrCosts.MaintRepairCost += (vehRevision.Sales * impact.MaintRepairCost);

            // update applied sales & phase-in on the mfr
            double vehSales = vehRevision.Sales;
            mfrRevision.TechAppliedSales[tech.Index] += vehSales;
            TA.CheckPhaseIn(mfrRevision, tech, complianceYear);
        }

        #endregion

        #region < Logging CF >

        /// <summary>
        /// Writes the details of this compliance finding to a log file.
        /// </summary>
        public void LogFinding()
        {
            var logWriter = this._compliance.LogWriter;
            if (!this.IsFinalIteration || !this.IsValid) {
                return;
            }

            var log = logWriter.GetCfLog(this._scen.Index);
            if (log != null && this.IsApplied) {
                this.LogFinding(log);
            }
        }
        void LogFinding(BufferedComplianceWriter cw)
        {
            // examine the range of model years during which any vehicles were selected
            for (int i = 0; i < this.YearCount; i++) {
                var yearStr = (this.StartYear + i).ToString();

                if (this.IsComponentLevel) {
                    this._logLine.Clear();

                    this.LogBasicInfo(this.StartYear);
                    this.LogComplianceTotals();
                    this.AppendEmpty(21); // can't log by-reg-class values for components
                    this.LogComponentResults(yearStr, this._tech.Name);

                    cw.WriteValues(this._mfr.Index, this._logLine.ToArray());
                }

                var techSolution = this.TechSolutions[i];
                var vehYears = techSolution.ApplicationYears;
                var impacts  = techSolution.TechImpacts;
                var testVehs = techSolution.TestVehicles;

                for (int j = 0; j < testVehs.Count; j++) {
                    this._logLine.Clear();

                    this.LogBasicInfo(vehYears[j]);
                    this.LogComplianceTotals();
                    this.LogComplianceStatsByRegClass(testVehs[j].RegClass);
                    this.LogVehicleResults(yearStr, impacts[j], testVehs[j]);

                    cw.WriteValues(this._mfr.Index, this._logLine.ToArray());
                }
            }
        }

        void LogBasicInfo(int applicationYear)
        {
            string cfID = (this._forcedTech) ? "F" : (this._doInherit) ? "I" : this.ID;
            bool isInherited = (this._doInherit || applicationYear != this.StartYear);

            this.AppendLine(cfID);
            this.AppendLine(this._scen.Index);
            this.AppendLine(this._year);
            this.AppendLine(this._mfr.Name);
            this.AppendLine((!this.IsApplied     ) ? string.Empty :
                            (this._forcedTech    ) ? "F" :
                            (isInherited         ) ? "I" :
                            (this.IsOvercomplying) ? "O" : "A");
            this.AppendLine(this.EfficiencyCAFE, NumberFormat);
            this.AppendLine(this.EfficiencyCO2, NumberFormat);
            this.AppendLine(GetComplianceState(this.IsInComplianceCAFE, this.IsOvercomplyingCAFE));
            this.AppendLine(GetComplianceState(this.IsInComplianceCO2, this.IsOvercomplyingCO2));
            this.AppendLine(this.BindingProgram?.ToString());

            // local helper function
            string GetComplianceState(bool isInCompliance, bool isOvercomplying)
            {
                return isOvercomplying ? "Over Complying"
                     : isInCompliance ? "In Compliance" : "Under Complying";
            }
        }
        void LogComplianceTotals()
        {
            this.AppendLine(this._totalAffectedSales   , NumberFormat);
            this.AppendLine(this._totalTechCost        , NumberFormat);
            this.AppendLine(this._totalTaxCredit       , NumberFormat);
            this.AppendLine(this._totalBatteryTaxCredit, NumberFormat);
            this.AppendLine(this._totalFuelSavings     , NumberFormat);
            this.AppendLine(this._deltaCAFECredits     , NumberFormat);
            this.AppendLine(this._deltaCAFECreditsKGal , NumberFormat);
            this.AppendLine(this._deltaCAFECreditValue , NumberFormat);
            this.AppendLine(this._deltaCAFECrValueBound, NumberFormat);
            this.AppendLine(this._deltaCAFEFines       , NumberFormat);
            this.AppendLine(this._deltaCO2Credits      , NumberFormat);
            this.AppendLine(this._deltaCO2CreditValue  , NumberFormat);
            this.AppendLine(this._deltaCO2CrValueBound , NumberFormat);
        }
        void LogComplianceStatsByRegClass(RegulatoryClass regClass)
        {
            // get compliance stats **for the analysis year** from the tech solution
            var curStats = this.TechSolutions[this._year - this.StartYear].CurrentStats;
            var newStats = this.TechSolutions[this._year - this.StartYear].NewStats;

            this.AppendLine(regClass);

            // CAFE (current stats)
            this.AppendLine(curStats.Standard   [regClass], NumberFormat);
            this.AppendLine(curStats.CAFERating [regClass], NumberFormat);
            this.AppendLine(curStats.Credits    [regClass], NumberFormat);
            this.AppendLine(curStats.CreditsKGal[regClass], NumberFormat);
            this.AppendLine(curStats.CreditValue[regClass], NumberFormat);
            this.AppendLine(curStats.Fines      [regClass], NumberFormat);

            // CAFE (new stats)
            this.AppendLine(newStats.Standard   [regClass], NumberFormat);
            this.AppendLine(newStats.CAFERating [regClass], NumberFormat);
            this.AppendLine(newStats.Credits    [regClass], NumberFormat);
            this.AppendLine(newStats.CreditsKGal[regClass], NumberFormat);
            this.AppendLine(newStats.CreditValue[regClass], NumberFormat);
            this.AppendLine(newStats.Fines      [regClass], NumberFormat);

            // CO2 (current stats)
            this.AppendLine(curStats.CO2Standard   [regClass], NumberFormat);
            this.AppendLine(curStats.CO2Rating     [regClass], NumberFormat);
            this.AppendLine(curStats.CO2Credits    [regClass], NumberFormat);
            this.AppendLine(curStats.CO2CreditValue[regClass], NumberFormat);

            // CO2 (new stats)
            this.AppendLine(newStats.CO2Standard   [regClass], NumberFormat);
            this.AppendLine(newStats.CO2Rating     [regClass], NumberFormat);
            this.AppendLine(newStats.CO2Credits    [regClass], NumberFormat);
            this.AppendLine(newStats.CO2CreditValue[regClass], NumberFormat);
        }
        void LogVehicleResults(string year, TechImpact impact, TestVehicle testVeh)
        {
            var veh = testVeh.Vehicle;
            var vehRevision = testVeh.Revision;
            var vehBaseInfo = veh.BaselineInfo;

            var settings = this.Settings;
            var gliderShare = settings.Technologies.GliderShare[vehRevision.TechnologyClass.GetArrayIndex()];

            this.AppendLine(year);
            this.AppendLine(impact.Technology.Name);

            // veh info
            this.AppendLine(testVeh.Sales);
            this.AppendLine(vehBaseInfo.Code);
            this.AppendLine(veh.Platform == null ? -1 : veh.Platform.BaselineInfo.Code);
            this.AppendLine(veh.Engine == null ? -1 : veh.Engine.BaselineInfo.Code);
            this.AppendLine(veh.Transmission == null ? -1 : veh.Transmission.BaselineInfo.Code);
            this.AppendLine(vehRevision.TechnologyClass.ToFriendlyString());
            this.AppendLine(vehRevision.EngineTechnologyClass.ToFriendlyString());
            this.AppendLine(vehBaseInfo.CurbWeightMR0, "0.##");
            this.AppendLine(vehBaseInfo.CurbWeightMR0 * gliderShare, "0.##"); // glider weight @ MR0

            // veh FE/CO2 changes
            var prevVeh = testVeh.PreviousState;
            var curFE   = prevVeh.FuelEconomy;
            var curCO2  = Standards.GetVehicleCO2Rating(this._scen, this._year, prevVeh, settings);
            var curFS   = prevVeh.FuelShare;
            var curFT   = curFS.FuelType;
            var newFE   = testVeh.FuelEconomy;
            var newCO2  = Standards.GetVehicleCO2Rating(this._scen, this._year, testVeh, settings);
            var newFS   = testVeh.FuelShare;
            var newFT   = newFS.FuelType;
            var dFE     = newFE  - curFE;
            var dCO2    = newCO2 - curCO2;

            this.AppendLine(curFE .ToString(NumberFormat, curFT));
            this.AppendLine(curCO2.ToString(NumberFormat, curFT));
            this.AppendLine(curFS .ToString(NumberFormat, curFT));
            this.AppendLine(newFE .ToString(NumberFormat, newFT));
            this.AppendLine(newCO2.ToString(NumberFormat, newFT));
            this.AppendLine(newFS .ToString(NumberFormat, newFT));
            this.AppendLine(dFE   .ToString(NumberFormat, curFT | newFT));
            this.AppendLine(dCO2  .ToString(NumberFormat, curFT | newFT));

            // tech impacts
            this.AppendLine(impact.TechCost     , NumberFormat);
            this.AppendLine(impact.BatteryCost  , NumberFormat);
            this.AppendLine(impact.FE1Adjustment, NumberFormatExt);
            this.AppendLine(impact.FE2Adjustment, NumberFormatExt);
            this.AppendLine(impact.DeltaCW      , NumberFormat);
            this.AppendLine(impact.DeltaGVWR    , NumberFormat);
            this.AppendLine(impact.DeltaGCWR    , NumberFormat);

            // tech keys
            this.AppendLine(TI.KeyToString(prevVeh.TechKey, ';'));
            this.AppendLine(TI.KeyToString(testVeh.TechKey, ';'));

            // tech effects for "ref" and "new" tech-keys
            this.AppendLine(impact.FE1ImprvRef   , NumberFormat);
            this.AppendLine(impact.FE2ImprvRef   , NumberFormat);
            this.AppendLine(impact.BatteryCostRef, NumberFormat);
            this.AppendLine(impact.FE1ImprvNew   , NumberFormat);
            this.AppendLine(impact.FE2ImprvNew   , NumberFormat);
            this.AppendLine(impact.BatteryCostNew, NumberFormat);
        }
        void LogComponentResults(string year, string techName)
        {
            // NOTES:
            // data cols for components are located among:
            //      Eval.Year, Techs, Sales, Veh, Eng, Trn, Plt, ...
            //
            // component level sales are sum across vehicles, but leave blank for now
            // leave "veh" col blank
            // populate appropriate component col, leave others blank
            // all other veh-level columns should be left blank

            this.AppendLine(year);
            this.AppendLine(techName);

            this.AppendLine(string.Empty); // Sales
            this.AppendLine(string.Empty); // Veh

            string componentCode = this._component?.BaselineInfo.Code.ToString();
            this.AppendLine((this._component is IPlatform) ? componentCode : string.Empty);
            this.AppendLine((this._component is IEngine) ? componentCode : string.Empty);
            this.AppendLine((this._component is ITransmission) ? componentCode : string.Empty);

            // currently 27 veh-level cols should be left blank
            this.AppendEmpty(27);
        }

        void AppendLine(double value, string format)
        {
            this.AppendLine(value.ToString(format));
        }
        void AppendLine(object value)
        {
            this._logLine.Add(value);
        }
        void AppendEmpty(int count)
        {
            for (int i = 0; i < count; i++) {
                this._logLine.Add(string.Empty);
            }
        }

        #endregion

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets the unique identifier associated with this compliance finding.</summary>
        public string ID
        {
            get => $"CF-{this._mfr.Code:00}.{this._tech.Index:00}.{this._version}";
        }

        /// <summary>Gets the regulatory classes with at least one vehicle model affected by pending technologies.</summary>
        public RCValue<bool> AffectedRC { get; private set; }
        /// <summary>Gets whether the compliance finding contains at least one vehicle model affected by pending
        ///     technologies.</summary>
        public bool HasVehs { get; private set; }

        /// <summary>Gets the earliest model year of initial technology application for at least one affected vehicle
        ///     model.</summary>
        public int StartYear { get; private set; }
        /// <summary>Gets the last model year with technology application for at least one affected vehicle model.</summary>
        public int EndYear { get; private set; }
        /// <summary>Gets the number of model years with valid technology solutions, where each model year has at least
        ///     one vehicle model affected by application of pending technologies.</summary>
        public int YearCount { get; private set; }

        /// <summary>
        /// Gets the technology solutions associated with application of pending technologies on one or more vehicle
        /// models, for a range of model years between specific start and end years.
        /// <br/>
        /// The first technology solution in the array corresponds to the <see cref="StartYear"/> associated with this
        /// compliance finding, which also represents the earliest year of initial technology application for at least
        /// one affected vehicle model. In contrast, the last <b>valid</b> technology solution corresponds to the
        /// <see cref="EndYear"/> of this compliance finding, which is typically the analysis year presently being
        /// evaluated by the compliance model.
        /// <br/>
        /// The number of valid solutions in the array is determined by the <see cref="YearCount"/> property of this
        /// compliance finding.
        /// </summary>
        public TechSolution[] TechSolutions { get; private set; }

        /// <summary>
        /// Gets whether the manufacturer is configured to prefer to pay CAFE fines instead of apply non-cost-effective
        /// technologies.
        /// </summary>
        public bool PreferCafeFines { get; private set; }

        /// <summary>
        /// Specifies the compliance program (CAFE, CO2, Both, or neither) that this compliance finding solution is in
        /// response to.
        /// </summary>
        public ComplianceProgram? BindingProgram { get; private set; }

        /// <summary>
        /// Gets whether the manufacturer is in compliance with the CAFE standards before application of pending
        /// technologies.
        /// </summary>
        public bool IsInComplianceCAFE { get; private set; }
        /// <summary>
        /// Gets whether the manufacturer is in compliance with the CO2 standards before application of pending
        /// technologies.
        /// </summary>
        public bool IsInComplianceCO2 { get; private set; }
        /// <summary>
        /// Gets whether the manufacturer is in compliance with both CAFE and CO2 standards before application of
        /// pending technologies.
        /// </summary>
        public bool IsInCompliance => this.IsInComplianceCAFE && this.IsInComplianceCO2;

        /// <summary>
        /// Gets whether the manufacturer is applying pending technologies for "over-compliance" with the CAFE
        /// standards.
        /// </summary>
        public bool IsOvercomplyingCAFE { get; private set; }
        /// <summary>
        /// Gets whether the manufacturer is applying pending technologies for "over-compliance" with the CO2
        /// standards.
        /// </summary>
        public bool IsOvercomplyingCO2 { get; private set; }
        /// <summary>
        /// Gets whether the manufacturer is applying pending technologies for "over-compliance" with both CAFE and CO2
        /// standards.
        /// </summary>
        public bool IsOvercomplying => this.IsOvercomplyingCAFE && this.IsOvercomplyingCO2;

        /// <summary>
        /// Gets the <see cref="Efficiency"/> metric calculated from the perspective of the CAFE compliance program.
        /// </summary>
        public double EfficiencyCAFE { get; private set; }
        /// <summary>
        /// Gets the <see cref="Efficiency"/> metric calculated from the perspective of the CO-2 compliance program.
        /// </summary>
        public double EfficiencyCO2 { get; private set; }
        /// <summary>
        /// Gets the efficiency rating (also referred to as "effective-cost") of this compliance finding.
        /// </summary>
        /// <remarks>
        /// If the value of the efficiency rating is <see cref="double.NaN"/>, the compliance finding is considered
        /// invalid and should be discarded. Invalid findings will throw an exception when a call to
        /// <see cref="ApplyFinding"/> is made.
        /// </remarks>
        public double Efficiency { get; private set; }

        /// <summary>
        /// Gets whether this compliance finding is efficient.
        /// </summary>
        /// <remarks>
        /// A compliance finding is considered efficient if its efficiency rating is less than 0. Efficiency values of
        /// greater than or equal to 0 are considered inefficient. Invalid compliance findings are always considered
        /// inefficient.
        /// </remarks>
        public bool IsEfficient => (this.Efficiency < 0);
        /// <summary>
        /// Gets whether this compliance finding is valid. A compliance finding is valid if <see cref="CalcEfficiency"/>
        /// method has been called (since the last refresh) and the resultant efficiency ratings represents a valid
        /// value.
        /// </summary>
        /// <remarks>
        /// If, during <see cref="CalcEfficiency"/> calculation, the fines to the manufacturer do not change as a
        /// result of the available technology's impact, or if it turns out that the compliance finding does not have
        /// any vehicles, the efficiency calculation will be assigned a value of <see cref="double.NaN"/>.
        /// </remarks>
        public bool IsValid => !double.IsNaN(this.Efficiency);

        /// <summary>Gets whether this compliance finding has been applied.</summary>
        public bool IsApplied { get; private set; }

        /// <summary>Gets whether this compliance finding represents a multiyear solution.</summary>
        public bool IsMultiYear => (this.YearCount > 1);

        private bool IsFinalIteration
        {
            get
            {
                var runtimeSettings = this.RuntimeSettings;
                return (!runtimeSettings.DynamicEconomicModeling || this._iter == runtimeSettings.DfsSrIterations);
            }
        }

        private bool IsComponentLevel => (this._component != null);

        private ModelingSettings Settings => this._compliance.Settings;
        private RuntimeSettings RuntimeSettings => this.Settings.RuntimeSettings;

        #endregion

        #region <<< Variables >>>

        //----- const - number formats for logging -----
        const string NumberFormat    = "0.####";
        const string NumberFormatExt = "0.########";
        
        //----- input variables -----
        ICompliance    _compliance;
        Scenario       _scen;
        int            _iter;
        int            _year;
        IManufacturer  _mfr;
        TechnologyInfo _tech;

        // control flags (indicates if "regular" finding, or if inheriting or forcing techs)
        bool _doInherit;  // indicates if model is inheriting a previous technology
        bool _forcedTech; // indicates if model is forcing application of a technology

        //----- runtime variables -----
        int _version; // the version of this compliance finding

        // component selected for evaluation (for component-level techs only)
        IOEMComponent _component;

        // list of vehicle/application-year combinations selected for evaluation
        List<AffectedVehicle> _affectedVehs;

        // compliance totals
        double _totalAffectedSales;
        double _totalTechCost;
        double _totalTaxCredit;
        double _totalBatteryTaxCredit;
        double _totalFuelSavings;

        double _deltaCAFECredits;
        double _deltaCAFECreditsKGal;
        double _deltaCAFECreditValue;
        double _deltaCAFECrValueBound;
        double _deltaCAFEFines;

        double _deltaCO2Credits;
        double _deltaCO2CreditValue;
        double _deltaCO2CrValueBound;

        // pre-allocated "log-line" is used for logging compliance finding results
        List<object> _logLine = new List<object>();

        #endregion

    }
}
