using System;
using System.Collections.Generic;
using System.Linq;

using Volpe.Cafe.Generic;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.Utils;

namespace Volpe.Cafe.IO.InputParsers
{
    /// <summary>
    /// Provides an input parser for loading and parsing Excel Parameters files.
    /// </summary>
    sealed class XlParametersParser : XlParser
    {

        #region /*** Ctors ***/

        internal XlParametersParser(string path) : base(path) { }

        #endregion

        #region /*** Methods ***/

        protected override void ParseInternal()
        {
            // begin parsing ...
            string[] sheets = this.SheetNames;
            string[] names = new string[] {
                "Economic Values",          //  0
                "Vehicle Age Data",         //  1
                "Fuel Prices",              //  2
                "DFS Model Values",         //  3
                "Scrappage Model Values",   //  4
                "Historic Fleet Data",      //  5
                "Safety Values",            //  6
                "Fatality Rates",           //  7
                "Credit Trading Values",    //  8
                "ZEV Credit Values",        //  9
                "Employment Values",        // 10
                "Fuel Properties",          // 11
                "Fuel Import Assumptions",  // 12
                "Emission Health Impacts",  // 13
                "Greenhouse Emission Costs",// 14
                "Criteria Emission Costs",  // 15
                "BTW_Emissions",            // 16
                "Sales Model Values"        // 17
            };
            string[] ueNames   = new string[] { "UE_Gasoline", "UE_Ethanol85", "UE_Diesel", "UE_Electricity", "UE_Hydrogen", "UE_CNG" };
            string[] teNames   = new string[] { "TE_Gasoline", "TE_Diesel" };
            int   [] indexes   = new int[names  .Length];
            int   [] ueIndexes = new int[ueNames.Length];
            int   [] teIndexes = new int[teNames.Length];
            for (int i = 0; i < indexes  .Length; i++) { indexes  [i] = -1; }
            for (int i = 0; i < ueIndexes.Length; i++) { ueIndexes[i] = -1; }
            for (int i = 0; i < teIndexes.Length; i++) { teIndexes[i] = -1; }

            // scan each worksheet name, looking for a match
            for (int i = 0; i < sheets.Length; i++)
            {
                for (int j = 0; j < names.Length; j++)
                {
                    if (indexes[j] == -1 && StringHelper.Equals(sheets[i], names[j], fuzzy: true)) { indexes[j] = i; break; }
                }
                for (int j = 0; j < ueNames.Length; j++)
                {
                    if (ueIndexes[j] == -1 && StringHelper.Equals(sheets[i], ueNames[j], fuzzy: true)) { ueIndexes[j] = i; break; }
                }
                for (int j = 0; j < teNames.Length; j++)
                {
                    if (teIndexes[j] == -1 && StringHelper.Equals(sheets[i], teNames[j], fuzzy: true)) { teIndexes[j] = i; break; }
                }
            }

            // check if all sheets were found
            if (!this.VerifyIndexes("Parameters workbook", indexes  , names  )) { return; }
            if (!this.VerifyIndexes("Parameters workbook", ueIndexes, ueNames)) { return; }
            if (!this.VerifyIndexes("Parameters workbook", teIndexes, teNames)) { return; }

            //---------------------------------------------//
            // Nothing is missing -- proceed to parse data //
            //---------------------------------------------//
            var economicValues          = this.ParseEconomicValues         (indexes[ 0]);
            var survivalRates           = this.ParseVehicleAgeData         (indexes[ 1], 1);
            var milesDriven             = this.ParseVehicleAgeData         (indexes[ 1], 1 + VehicleStyles.AltList.Length);
            var fuelPrices              = this.ParseFuelPrices             (indexes[ 2]);
            var dfsModelValues          = this.ParseDFSModelValues         (indexes[ 3]);
            var scrappageModelValues    = this.ParseScrappageModelValues   (indexes[ 4]);
            var historicFleetData       = this.ParseHistoricFleetData      (indexes[ 5]);
            var safetyValues            = this.ParseSafetyValues           (indexes[ 6]);
            var fatalityRates           = this.ParseFatalityRates          (indexes[ 7], names[7]);
            var creditTradingValues     = this.ParseCreditTradingValues    (indexes[ 8]);
            var zevCreditValues         = this.ParseZEVCreditValues        (indexes[ 9]);
            var employmentValues        = this.ParseEmploymentValues       (indexes[10]);
            var fuelProperties          = this.ParseFuelProperties         (indexes[11]);
            var importAssumptions       = this.ParseFuelImportAssumptions  (indexes[12], names[12]);
            var emissionHealthImpacts   = this.ParseEmissionHealthImpacts  (indexes[13]);
            var greenhouseEmissionCosts = this.ParseGreenhouseEmissionCosts(indexes[14]);
            var criteriaEmissionCosts   = this.ParseCriteriaEmissionCosts  (indexes[15]);
            var brakeTireWearEmissions  = this.ParseBrakeTireWearEmissions (indexes[16]);
            var salesModelValues        = this.ParseSalesModelValues       (indexes[17]);
            var upstreamEmissions       = this.ParseUpstreamEmissions      (ueIndexes, ueNames);
            var tailpipeGasoline        = this.ParseTailpipeEmissions      (teIndexes[0], teNames[0]);
            var tailpipeDiesel          = this.ParseTailpipeEmissions      (teIndexes[1], teNames[1]);

            this._value = new Parameters(new Dictionary<string, object> {
                ["EconomicValues"] = economicValues,
                ["SurvivalRates"] = survivalRates,
                ["MilesDriven"] = milesDriven,
                ["FuelPrices"] = fuelPrices,
                ["HistoricFleetData"] = historicFleetData,
                ["DfsModelValues"] = dfsModelValues,
                ["SalesModelValues"] = salesModelValues,
                ["ScrappageModelValues"] = scrappageModelValues,
                ["SafetyValues"] = safetyValues,
                ["FatalityRates"] = fatalityRates,
                ["CreditTradingValues"] = creditTradingValues,
                ["ZEVCreditValues"] = zevCreditValues,
                ["EmploymentValues"] = employmentValues,
                ["FuelProperties"] = fuelProperties,
                ["FuelImportAssumptions"] = importAssumptions,
                ["EmissionHealthImpacts"] = emissionHealthImpacts,
                ["GreenhouseEmissionCosts"] = greenhouseEmissionCosts,
                ["CriteriaEmissionCosts"] = criteriaEmissionCosts,
                ["BrakeTireWearEmissions"] = brakeTireWearEmissions,
                ["UpstreamEmissions"] = upstreamEmissions,
                ["TailpipeGasoline"] = tailpipeGasoline,
                ["TailpipeDiesel"] = tailpipeDiesel
            });
        }

        EconomicValues ParseEconomicValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);
            int row = 2, col = 1;

            // parse disc rates
            double[] discountRate     = this.GetDiscRates(++row, col, "social");
            int      drBaseYear       = this.GetInt32    (++row, col);
            double[] consumerDiscRate = this.GetDiscRates(++row, col, "consumer");

            // co2 disc rates
            ++row; // skip CO2 discount rate header
            int numRates = Enum.GetValues(typeof(Estimates)).Length;
            var co2DiscountRates = new List<double>(numRates);
            for (int dr = 0; dr < numRates; ++dr)
            {
                co2DiscountRates.Add(this.GetDouble(++row, col));
            }

            // parse 2012-USD deflator
            ++row; // skip header
            double deflator2012 = this.GetDouble(++row, col);

            // parse other values by reg-class
            var parameters     = new VSValue<EconomicParameters>();
            var externalCosts  = new VSValue<ExternalCosts>();
            var operatingCosts = new VSValue<OperatingCosts>();
            //
            int ftCount = FuelTypes.List.Length;
            int loopStartRow = row + 2;
            for (int i = 0; i < parameters.Items.Length; i++)
            {
                row = loopStartRow;
                //
                parameters    .Items[i] = this.ParseEconomicParameters(++row, col); row += 23 + 2 * ftCount;
                externalCosts .Items[i] = this.ParseExternalCosts     (++row, col); row += 2;
                operatingCosts.Items[i] = this.ParseOperatingCosts    (++row, col); row += 6;
                //
                col++;
            }

            // parse economic costs (these are not by reg-class)
            var monopsony         = new List<double>();
            var priceShock        = new List<double>();
            var milSecurity       = new List<double>();
            var gdp               = new List<double>();
            var numHouseholds     = new List<double>();
            var consumerSentiment = new List<double>();
            var rdpi              = new List<double>();
            var usPopulation      = new List<double>();
            var historicVMT       = new List<double>();
            var historicMPG       = new List<double>();

            int minCY = 0, maxCY = 0, prevYear = -1;

            for (row += 3; row < this.Rows; row++)
            {
                string errHDR = "Economic Values (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                if (!ModelYear.IsValid(year)) { this.LogError(errHDR + "Calendar year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Calendar years are not contiguous."); }
                prevYear = year;

                if (minCY == 0) { minCY = year; }
                else            { maxCY = year; }

                col = 1;

                monopsony        .Add(this.GetDouble(row, col++));
                priceShock       .Add(this.GetDouble(row, col++));
                milSecurity      .Add(this.GetDouble(row, col++));
                gdp              .Add(this.GetDouble(row, col++));
                numHouseholds    .Add(this.GetDouble(row, col++));
                consumerSentiment.Add(this.GetDouble(row, col++));
                usPopulation     .Add(this.GetDouble(row, col++));
                rdpi             .Add(this.GetDouble(row, col++));
                historicVMT      .Add(this.GetDouble(row, col++));
                historicMPG      .Add(this.GetDouble(row, col++));
            }

            var economicCosts = new EconomicCosts(new Dictionary<string, object>
            {
                {"MinCY", minCY },
                {"MaxCY", maxCY },
                {"Monopsony", monopsony.ToArray() },
                {"PriceShock", priceShock.ToArray() },
                {"MilitarySecurity", milSecurity.ToArray() }
            });
            var macroParameters = new MacroeconomicParameters(new Dictionary<string, object>
            {
                {"MinCY", minCY },
                {"MaxCY", maxCY },
                {"GDP", gdp.ToArray() },
                {"NumHouseholds", numHouseholds.ToArray() },
                {"ConsumerSentiment", consumerSentiment.ToArray() },
                {"RDPI", rdpi.ToArray() },
                {"USPopulation", usPopulation.ToArray() }
            }); 
            var vmtModelParameters = new VMTModelParameters(new Dictionary<string, object>
            {
                {"MinCY", minCY },
                {"MaxCY", maxCY },
                {"HistoricVMT", historicVMT.ToArray() },
                {"HistoricMPG", historicMPG.ToArray() }
            });

            // build and return "economic values" object
            return new EconomicValues(new Dictionary<string, object>
            {
                {"DiscountRate", discountRate },
                {"DRBaseYear", drBaseYear },
                {"ConsumerDiscRate", consumerDiscRate },
                {"CO2DiscountRate", co2DiscountRates.ToArray() },
                {"Deflator2012", deflator2012 },
                {"Parameters", parameters },
                {"ExternalCosts", externalCosts },
                {"OperatingCosts", operatingCosts },
                {"EconomicCosts", economicCosts },
                {"MacroParameters", macroParameters },
                {"VMTModelParameters", vmtModelParameters }
            });
        }
        EconomicParameters ParseEconomicParameters(int sRow, int column)
        {
            int ftCount = FuelTypes.List.Length;

            double reboundEffect     = this.GetDouble        (sRow++, column);
            int    vmtBaseYear       = this.GetInt32         (sRow++, column); sRow += 1;
            var    onRoadGap         = this.ParseFuelValue   (sRow++, column); sRow += ftCount;
            var    refuelTime        = this.ParseFuelValue   (sRow++, column); sRow += ftCount - 1;
            double refuelTankVolume  = this.GetDouble        (sRow++, column);
            double refuelEventsScale = this.GetDouble        (sRow++, column);
            double travelValue       = this.GetDouble        (sRow++, column); sRow += 1;
            var    bev200Thresholds  = this.ParseEVThresholds(sRow++, column); sRow += 3;
            var    bev300Thresholds  = this.ParseEVThresholds(sRow++, column); sRow += 3;
            var    bev400Thresholds  = this.ParseEVThresholds(sRow++, column); sRow += 3;
            var    bev500Thresholds  = this.ParseEVThresholds(sRow++, column);

            return new EconomicParameters(new Dictionary<string, object> {
                ["ReboundEffect"     ] = reboundEffect,
                ["VMTBaseYear"       ] = vmtBaseYear,
                ["OnRoadGap"         ] = onRoadGap,
                ["RefuelTime"        ] = refuelTime,
                ["RefuelTankVolume"  ] = refuelTankVolume,
                ["RefuelEventsScale" ] = refuelEventsScale,
                ["VehicleTravelValue"] = travelValue,
                ["BEV200Thresholds"  ] = bev200Thresholds,
                ["BEV300Thresholds"  ] = bev300Thresholds,
                ["BEV400Thresholds"  ] = bev400Thresholds,
                ["BEV500Thresholds"  ] = bev500Thresholds
            });
        }
        ExternalCosts ParseExternalCosts(int sRow, int column)
        {
            return new ExternalCosts(new Dictionary<string, object>
            {
                { "Congestion", this.GetDouble(sRow++, column) },
                { "Noise"     , this.GetDouble(sRow  , column) }
            });
        }
        OperatingCosts ParseOperatingCosts(int sRow, int column)
        {
            return new OperatingCosts(new Dictionary<string, object>
            {
                { "TaxesAndFees"       , this.GetDouble(sRow++, column) },
                { "FinancingTerm"      , this.GetDouble(sRow++, column) },
                { "FinancingInterest"  , this.GetDouble(sRow++, column) },
                { "ShareFinanced"      , this.GetDouble(sRow++, column) },
                { "VehicleDepreciation", this.GetDouble(sRow++, column) },
                { "AverageResaleTime"  , this.GetInt32 (sRow++, column) }
            });
        }
        VehicleAgeData ParseVehicleAgeData(int sheetIndex, int sCol)
        {
            this.ActivateWorksheet(sheetIndex);

            double[] cars     = new double[Globals.CY];
            double[] vansSuvs = new double[Globals.CY];
            double[] pickups  = new double[Globals.CY];
            double[] hdpuvs   = new double[Globals.CY];

            for (int i = 0; i < Globals.CY; i++)
            {
                cars    [i] = this.GetDouble(5 + i, sCol + 0);
                vansSuvs[i] = this.GetDouble(5 + i, sCol + 1);
                pickups [i] = this.GetDouble(5 + i, sCol + 2);
                hdpuvs  [i] = this.GetDouble(5 + i, sCol + 3);
            }

            return new VehicleAgeData(cars, vansSuvs, pickups, hdpuvs);
        }
        FuelPrices ParseFuelPrices(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            var fuelTax   = new FTValue<double[]>();
            var fuelPrice = new FTValue<double[]>();
            int minCY     =  0;
            int maxCY     =  0;
            int prevYear  = -1;

            int startRow = 4;
            for (int row = startRow; row < this.Rows; row++)
            {
                string errHDR = "Fuel Prices (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                if (year < 1900) { this.LogError(errHDR + "Calendar year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Calendar years are not contiguous."); }
                prevYear = year;
                //
                if (minCY == 0) { minCY = year; }
                else            { maxCY = year; }
            }

            int yearCount = maxCY - minCY + 1;
            for (int i = 0; i < FuelTypes.List.Length; i++)
            {
                FuelType fuel = FuelTypes.List[i];
                fuelTax  [fuel] = new double[yearCount];
                fuelPrice[fuel] = new double[yearCount];

                for (int row = startRow; row < this.Rows; row++)
                {
                    if (this.GetString(row, 0).Length == 0)
                        continue;

                    fuelPrice[fuel][row - startRow] = Math.Round(this.GetDouble(row, 1 + 2 * i), 4);
                    fuelTax  [fuel][row - startRow] = Math.Round(this.GetDouble(row, 2 + 2 * i), 4);
                }
            }

            return new FuelPrices(new Dictionary<string, object>
            {
                { "MinCY"    , minCY     },
                { "MaxCY"    , maxCY     },
                { "FuelTax"  , fuelTax   },
                { "FuelPrice", fuelPrice }
            });
        }
        HistoricFleetData ParseHistoricFleetData(int sheetIndex)
        {
            int historicDataStartRow = 4;
            int vsCount = VehicleStyles.AltList.Length;
            int ftCount = FuelTypes.List.Length;

            this.ActivateWorksheet(sheetIndex);
            this.ValidateModelYears("Historic Fleet Data", historicDataStartRow, out int minMY, out int maxMY, out int yearCount);

            var historicData = new VSValue<HistoricFleetDataItem[]>();
            for (int i = 0; i < vsCount; i++)
            {
                historicData.Items[i] = new HistoricFleetDataItem[yearCount];
            }

            for (int row = historicDataStartRow; row < this.Rows; row++)
            {
                if (this.GetString(row, 0).Length == 0) { continue; }
                //
                int col = 2;
                for (int i = 0; i < vsCount; i++)
                {
                    double initProd     = this.GetDouble      (row, col++);
                    double onRoadFleet  = this.GetDouble      (row, col++);
                    double pcShare      = this.GetDouble      (row, col++);
                    var onRoadFE        = this.ParseFuelValueH(row, col  ); col += ftCount;
                    var fuelShare       = this.ParseFuelValueH(row, col  ); col += ftCount;
                    double horsepower   = this.GetDouble      (row, col++);
                    double curbWeight   = this.GetDouble      (row, col++);
                    double fuelCapacity = this.GetDouble      (row, col++);
                    double price        = this.GetDouble      (row, col++);

                    historicData.Items[i][row - historicDataStartRow] = new HistoricFleetDataItem(new Dictionary<string, object>
                    {
                        { "InitialProduction", initProd     },
                        { "OnRoadFleet"      , onRoadFleet  },
                        { "PCShare"          , pcShare      },
                        { "OnRoadFE"         , onRoadFE     },
                        { "FuelShare"        , fuelShare    },
                        { "Horsepower"       , horsepower   },
                        { "CurbWeight"       , curbWeight   },
                        { "FuelCapacity"     , fuelCapacity },
                        { "TransactionPrice" , price        }
                    });
                }
            }

            return new HistoricFleetData(new Dictionary<string, object>
            {
                { "MinMY"       , minMY         },
                { "MaxMY"       , maxMY         },
                { "HistoricData", historicData  }
            });
        }
        DynamicFleetShareModelValues ParseDFSModelValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            const int DfsCoefficientCount = 10;
            const int BoundingCoefficientCount = 3;

            var dfsCoefficients = new double[DfsCoefficientCount];
            var boundingCoefficients = new double[BoundingCoefficientCount];
            int row = 3;

            for (int i = 0; i < dfsCoefficients.Length; i++)
            {
                dfsCoefficients[i] = this.GetDouble(row++, 1);
            }
            row += 2; // 2-row gap between dfs and bounding coefs
            for (int i = 0; i < boundingCoefficients.Length; i++)
            {
                boundingCoefficients[i] = this.GetDouble(row++, 1);
            }

            // load annual forecast of car shares
            row += 3; // adjust for 3-row gap

            int minYear = 0, maxYear = 0, prevYear = -1;
            var staticCarShareValues = new List<double>();

            for (; row < this.Rows; row++)
            {
                string errHDR = "DFS Model Values (R:" + (row + 1).ToString("0000") + "):  ";
                int year = this.GetInt32(row, 0);
                if (!ModelYearKey.IsValid(year)) {
                    this.LogError(errHDR + "Model year is not valid.");
                }
                if (prevYear != -1 && prevYear + 1 != year) {
                    this.LogError(errHDR + "Model years must be sequential.");
                }
                prevYear = year;

                if (minYear == 0) {
                    minYear = year;
                }
                else {
                    maxYear = year;
                }

                staticCarShareValues.Add(this.GetDouble(row, 1));
            }

            var staticCarShares = new ReadOnlyAnnualValue<double>(minYear, maxYear, staticCarShareValues);

            return new DynamicFleetShareModelValues(new Dictionary<string, object> {
                ["DfsCoefficients"] = dfsCoefficients,
                ["BoundingCoefficients"] = boundingCoefficients,
                ["StaticCarShares"] = staticCarShares
            });
        }
        SalesModelValues ParseSalesModelValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            const int NfCoefficientCount = 8;
            var nfCoefficientsLD = new double[NfCoefficientCount];
            var nfCoefficientsHDPUV = new double[NfCoefficientCount];
            int row = 4;

            for (int i = 0; i < nfCoefficientsLD.Length; i++) {
                nfCoefficientsLD[i] = this.GetDouble(row, 1);
                nfCoefficientsHDPUV[i] = this.GetDouble(row, 2);
                row++;
            }

            // load annual forecast of sales
            row += 3; // adjust for 3-row gap

            int minYear = 0, maxYear = 0, prevYear = -1;
            var annualForecastValuesLD = new List<double>();
            var annualForecastValuesHDPUV = new List<double>();

            for (; row < this.Rows; row++) {
                string errHDR = "Sales Model Values (R:" + (row + 1).ToString("0000") + "):  ";
                int year = this.GetInt32(row, 0);
                if (!ModelYearKey.IsValid(year)) {
                    this.LogError(errHDR + "Model year is not valid.");
                }
                if (prevYear != -1 && prevYear + 1 != year) {
                    this.LogError(errHDR + "Model years must be sequential.");
                }
                prevYear = year;

                if (minYear == 0) {
                    minYear = year;
                }
                else {
                    maxYear = year;
                }

                annualForecastValuesLD.Add(this.GetDouble(row, 1));
                annualForecastValuesHDPUV.Add(this.GetDouble(row, 2));
            }

            var annualForecastLD = new ReadOnlyAnnualValue<double>(minYear, maxYear, annualForecastValuesLD);
            var annualForecastHDPUV = new ReadOnlyAnnualValue<double>(minYear, maxYear, annualForecastValuesHDPUV);

            return new SalesModelValues(new Dictionary<string, object> {
                ["NFCoefficientsLD"] = nfCoefficientsLD,
                ["NFCoefficientsHDPUV"] = nfCoefficientsHDPUV,
                ["AnnualForecastLD"] = annualForecastLD,
                ["AnnualForecastHDPUV"] = annualForecastHDPUV
            });
        }
        DynamicScrappageModelValues ParseScrappageModelValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            int betaCoefficientCount = 17;
            var betaCoefficients = new VSValue<double[]>();

            for (int i = 0; i < betaCoefficients.Items.Length; i++)
            {
                int row = 3;
                int col = 1 + i;
                betaCoefficients .Items[i] = new double[betaCoefficientCount];
                for (int j = 0; j < betaCoefficients.Items[i].Length; j++)
                {
                    betaCoefficients.Items[i][j] = this.GetDouble(row++, col);
                }
            }

            return new DynamicScrappageModelValues(new Dictionary<string, object>
            {
                { "BetaCoefficients", betaCoefficients }
            });
        }
        SafetyValues ParseSafetyValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            // parameters by safety class
            var threshold   = new SCValue<double>();
            var smallEffect = new SCValue<double>();
            var largeEffect = new SCValue<double>();
            int row = 3;

            foreach (SafetyClass sftClass in SafetyClasses.List)
            {
                threshold  [sftClass] = this.GetDouble(row, 1);
                smallEffect[sftClass] = this.GetDouble(row, 2);
                largeEffect[sftClass] = this.GetDouble(row, 3);
                row++;
            }

            // safety costs table
            row += 3;
            var fatalityCosts = (Cost: this.GetDouble(row, 1), GrowthRate: this.GetDouble(row++, 2));
            var nonFatalInjuryCosts = (Cost: this.GetDouble(row, 1), GrowthRate: this.GetDouble(row++, 2));
            var propertyDamageCosts = (Cost: this.GetDouble(row, 1), GrowthRate: this.GetDouble(row++, 2));

            row++;
            int baseYear = this.GetInt32(row++, 2);
            double fatalityRisk = this.GetDouble(row, 2);

            // non-fatal injury series (by calendar year)
            var nonFatalInjuryCostScalar = new List<double>();
            var nonFatalInjuryMultiplier = new List<double>();
            int minCY = 0;
            int maxCY = 0;
            int prevYear = -1;

            for (row += 3; row < this.Rows; row++)
            {
                string errHDR = "Safety Values (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                if (!ModelYear.IsValid(year)) { this.LogError(errHDR + "Calendar year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Calendar years are not contiguous."); }
                prevYear = year;

                if (minCY == 0) { minCY = year; } else { maxCY = year; }

                nonFatalInjuryCostScalar.Add(this.GetDouble(row, 1));
                nonFatalInjuryMultiplier.Add(this.GetDouble(row, 2));
            }

            return new SafetyValues(new Dictionary<string, object>
            {
                { "Threshold", threshold },
                { "SmallEffect", smallEffect },
                { "LargeEffect", largeEffect },

                { "FatalityCosts", fatalityCosts },
                { "NonFatalInjuryCosts", nonFatalInjuryCosts },
                { "PropertyDamageCrashCosts", propertyDamageCosts },

                { "BaseYear", baseYear },
                { "FatalityRisk", fatalityRisk }
            });
        }
        FatalityRates ParseFatalityRates(int sheetIndex, string sheetName)
        {
            this.ActivateWorksheet(sheetIndex);

            // find min and max model years
            int minMY = -1, maxMY = -1;
            int prevYear = -1;
            int ageCounter = 0;

            for (int row = 3; row < this.Rows; row++)
            {
                string errHDR = sheetName + " (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                int age  = this.GetInt32(row, 2);
                if (year < 1900 || year > 2200) { this.LogError(errHDR + "Model year is not valid."); }
                if (age != (ageCounter++)) { this.LogError(errHDR + "Vehicle ages are not contiguous."); }
                if (ageCounter == 40) { ageCounter = 0; }
                if (ageCounter == 1 && prevYear != -1 && prevYear + 1 != year)
                {   // check for new model year at first age
                    if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Model years are not contiguous."); }
                }
                else if (ageCounter > 1 && prevYear != -1 && prevYear != year)
                {   // model year shouldn't change until vehicle age resets to zero
                    this.LogError(errHDR + "Model years are out of order.");
                }
                prevYear = year;

                if (minMY == -1) { minMY = year; } else { maxMY = year; }
            }

            // do not attempt to load input data if there were errors
            if (this.HasErrors)
                return null;

            // load fatality rates
            int yearCount = maxMY - minMY + 1;

            double[][] fatalityRateLow = new double[yearCount][];
            double[][] fatalityRateAvg = new double[yearCount][];
            double[][] fatalityRateHigh = new double[yearCount][];
            double[][] nonFatalRateLow = new double[yearCount][];
            double[][] nonFatalRateAvg = new double[yearCount][];
            double[][] nonFatalRateHigh = new double[yearCount][];
            double[][] propertyDamageRateLow = new double[yearCount][];
            double[][] propertyDamageRateAvg = new double[yearCount][];
            double[][] propertyDamageRateHigh = new double[yearCount][];

            for (int yr = 0; yr < yearCount; yr++)
            {
                fatalityRateLow[yr] = new double[Globals.CY];
                fatalityRateAvg[yr] = new double[Globals.CY];
                fatalityRateHigh[yr] = new double[Globals.CY];

                nonFatalRateLow[yr] = new double[Globals.CY];
                nonFatalRateAvg[yr] = new double[Globals.CY];
                nonFatalRateHigh[yr] = new double[Globals.CY];

                propertyDamageRateLow[yr] = new double[Globals.CY];
                propertyDamageRateAvg[yr] = new double[Globals.CY];
                propertyDamageRateHigh[yr] = new double[Globals.CY];

                for (int age = 0; age < Globals.CY; age++)
                {
                    int col = 3;

                    fatalityRateLow[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                    fatalityRateAvg[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                    fatalityRateHigh[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);

                    nonFatalRateLow[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                    nonFatalRateAvg[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                    nonFatalRateHigh[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);

                    propertyDamageRateLow[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                    propertyDamageRateAvg[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                    propertyDamageRateHigh[yr][age] = this.GetDouble(3 + yr * Globals.CY + age, col++);
                }
            }

            return new FatalityRates(new Dictionary<string, object>
            {
                { "MinMY", minMY },
                { "MaxMY", maxMY },

                { "FatalityRateLow", fatalityRateLow },
                { "FatalityRateAverage", fatalityRateAvg },
                { "FatalityRateHigh", fatalityRateHigh },

                { "NonFatalInjuryRateLow", nonFatalRateLow },
                { "NonFatalInjuryRateAverage", nonFatalRateAvg },
                { "NonFatalInjuryRateHigh", nonFatalRateHigh },

                { "PropertyDamageCrashRateLow", propertyDamageRateLow },
                { "PropertyDamageCrashRateAverage", propertyDamageRateAvg },
                { "PropertyDamageCrashRateHigh", propertyDamageRateHigh }
            });
        }
        CreditTradingValues ParseCreditTradingValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            bool allowTrading   = this.GetBool (3, 3);
            bool allowTransfers = this.GetBool (4, 3);
            bool allowCarryFwd  = this.GetBool (5, 3);
            int  carryFwdYears  = this.GetInt32(6, 3);
            bool allowCarryBwd  = this.GetBool (7, 3);
            int  carryBwdYears  = this.GetInt32(8, 3);

            // load "by-year" credit trading values
            var transferCaps = new RCValue<double[]>();
            var adjVMT       = new RCValue<double[]>();
            int minCY = 0, maxCY = 0, prevYear = -1;
            //
            for (int row = 12; row < this.Rows; row++)
            {
                string errHDR = "Credit Trading Values (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { break; }
                int year = this.GetInt32(row, 0);
                if (!ModelYear.IsValid(year)) { this.LogError(errHDR + "Calendar year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Calendar years are not contiguous."); }
                prevYear = year;
                //
                if (minCY == 0) { minCY = year; }
                else            { maxCY = year; }
            }
            //
            int yearCount = maxCY - minCY + 1;
            int rcCount = RegClasses.List.Length;
            for (int i = 0; i < rcCount; i++)
            {
                RegulatoryClass regClass = RegClasses.List[i];
                transferCaps[regClass] = new double[yearCount];
                adjVMT      [regClass] = new double[yearCount];

                for (int row = 12; row < this.Rows; row++)
                {
                    if (this.GetString(row, 0).Length == 0) { break; }
                    transferCaps[regClass][row - 12] = this.GetDouble(row, 1 + i);
                    adjVMT      [regClass][row - 12] = this.GetDouble(row, 1 + i + rcCount);
                }
            }
            //
            int maxExpiringYears = Math.Max(0, this.GetInt32(14 + yearCount, 3));

            return new CreditTradingValues(new Dictionary<string, object>
            {
                { "AllowMfrTrading", allowTrading},
                { "AllowFleetTransfers", allowTransfers},
                { "AllowCarryForward", allowCarryFwd},
                { "CarryForwardYears", carryFwdYears},
                { "AllowCarryBackward", allowCarryBwd},
                { "CarryBackwardYears", carryBwdYears},
                { "MinMY", minCY},
                { "TransferCaps",transferCaps },
                { "AdjustmentVMT", adjVMT},
                { "MaxExpiringYears", maxExpiringYears}
            });
        }
        ZEVCreditValues ParseZEVCreditValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            int minMY = 0, maxMY = 0, prevYear = -1;
            //
            for (int row = 3; row < this.Rows; row++)
            {
                string errHDR = "ZEV Credit Values (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                if (year < 1900) { this.LogError(errHDR + "Model year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Model years are not contiguous."); }
                prevYear = year;
                //
                if (minMY == 0) { minMY = year; }
                else { maxMY = year; }
            }

            int yearCount = maxMY - minMY + 1;
            double[] zevRequirementLD    = new double[yearCount];
            double[] phevCapLD           = new double[yearCount];
            double[] zevRequirementHDPUV = new double[yearCount];
            double[] phevCapHDPUV        = new double[yearCount];

            for (int row = 3; row < this.Rows; row++)
            {
                zevRequirementLD   [row - 3] = this.GetDouble(row, 1);
                phevCapLD          [row - 3] = this.GetDouble(row, 2);
                zevRequirementHDPUV[row - 3] = this.GetDouble(row, 3);
                phevCapHDPUV       [row - 3] = this.GetDouble(row, 4);
            }

            return new ZEVCreditValues(new Dictionary<string, object>
            {
                { "MinMY", minMY },
                { "MaxMY", maxMY },
                { "ZevRequirement", zevRequirementLD },
                { "PhevCap", phevCapLD },
                { "ZevRequirementHDPUV", zevRequirementHDPUV },
                { "PhevCapHDPUV", phevCapHDPUV }
            });
        }
        EmploymentValues ParseEmploymentValues(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            int    row              = 2;
            double oemRevenue       = this.GetDouble(row++, 1);
            double suplierRevenue   = this.GetDouble(row++, 1);
            double rpe              = this.GetDouble(row++, 1);
            double annualLaborHours = this.GetDouble(row++, 1);
            double usMultiplier     = this.GetDouble(row++, 1);
            double globalMultiplier = this.GetDouble(row  , 1);
            
            return new EmploymentValues(new Dictionary<string, object>
            {
                { "OEMRevenue", oemRevenue },
                { "SuplierRevenue", suplierRevenue },
                { "RPE", rpe },
                { "AnnualLaborHours", annualLaborHours },
                { "USAssemblyMultiplier", usMultiplier },
                { "GlobalMultiplier", globalMultiplier }
            });
        }
        FuelProperties ParseFuelProperties(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            FuelValue energyDensity = this.ParseFuelValue(3, 1);
            FuelValue massDensity   = this.ParseFuelValue(3, 2);
            FuelValue carbonContent = this.ParseFuelValue(3, 3);
            FuelValue co2Emissions  = massDensity * carbonContent * 44 / 12;
            FuelValue so2Emissions  = this.ParseFuelValue(3, 4);

            return new FuelProperties(new Dictionary<string, object>
            {
                { "EnergyDensity", energyDensity },
                { "MassDensity", massDensity },
                { "CarbonContent", carbonContent },
                { "CO2Emissions", co2Emissions },
                { "SO2Emissions", so2Emissions }
            });
        }
        FuelImportAssumptionsCollection ParseFuelImportAssumptions(int sheetIndex, string sheetName)
        {
            this.ActivateWorksheet(sheetIndex);

            // ensure worksheet structure is correct
            // import assumptions should be specified in blocks with calendar year as first row of
            // each block, followed by all supported fuel types, as such:
            //
            //                  Value1  Value2  Value3  Value4
            //  1995 ...........................................    <~~ | calendar year is used to
            //  Gasoline          1       2       3       4             | mark start of block only
            //  Ethanol-85        5       6       7       8             | actual values should be
            //  Diesel            9       0       1       2             | specified for fuel types
            //  Electricity       3       4       5       6
            //  Hydrogen          7       8       9       0
            //  CNG               1       2       3       4
            //  2005 ...........................................
            //  Gasoline          5       6       7       8
            //  Ethanol-85        9       0       1       2
            //  Diesel            3       4       5       6
            //  Electricity       7       8       9       0
            //  Hydrogen          1       2       3       4
            //  CNG               5       6       7       8
            string[] ftNames = new string[] { "Gasoline", "Ethanol85", "Diesel", "Electricity", "Hydrogen", "CNG" };
            var years = new List<int>();
            var values = new List<FTValue<FuelImportAssumptions>>();
            //
            for (int row = 3; row < this.Rows; /*row++*/)
            {
                string errHDR = sheetName + " (R:" + (row + 1).ToString("0000") + "):  ";

                // start new block with calendar year
                int year = this.GetInt32(row, 0);
                //
                if (years.Contains(year)) { this.LogError(errHDR + "Duplicate calendar years are not allowed."); }
                else if (year < 1900 || year > 2200) { this.LogError(errHDR + "Calendar year is not valid."); }
                else
                {   // scan block for all fuel types
                    row++;
                    int ftCount = 0;
                    for (int ft = 0; ft < ftNames.Length; ft++)
                    {
                        string fuel = this.GetString(row + ft, 0);
                        if (!StringHelper.Equals(fuel, ftNames[ft], fuzzy: true))
                        {
                            this.LogError(errHDR + "At CY-" + year + " block, fuel types are not in correct order.");
                        }
                        else { ftCount++; }
                    }
                    if (ftCount != ftNames.Length)
                    {
                        this.LogError(errHDR + "The CY-" + year + " block is missing one or more fuel type entries.");
                    }
                    else
                    {   // if all matches, load import assumptions for this CY
                        years.Add(year);
                        FTValue<FuelImportAssumptions> value = new FTValue<FuelImportAssumptions>();
                        for (int ft = 0; ft < ftNames.Length; ft++)
                        {
                            value.Items[ft] = new FuelImportAssumptions(new Dictionary<string, object>
                            {
                                { "FuelSavingsLeadingToLowerImports"    , this.GetDouble(row + ft, 1) },
                                { "FuelSavingsLeadingToReducedRefining" , this.GetDouble(row + ft, 2) },
                                { "ReducedRefiningFromDomesticCrude"    , this.GetDouble(row + ft, 3) },
                                { "ReducedRefiningFromImportedCrude"    , this.GetDouble(row + ft, 4) }
                            });
                        }
                        values.Add(value);
                    }
                    row += ftCount;
                }
            } // next row

            return new FuelImportAssumptionsCollection(
                new ValueByYear<FTValue<FuelImportAssumptions>>(years.ToArray(), values.ToArray()));
        }
        EmissionHealthImpacts ParseEmissionHealthImpacts(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            var years = new List<int>();
            var values = new Dictionary<EmissionHealthImpact, List<CriteriaEmissionsValues>>();
            foreach (var ehi in HealthImpacts.List)
            {
                values.Add(ehi, new List<CriteriaEmissionsValues>());
            }

            int startRow = 4;
            int numRowsInSection = HealthImpacts.List.Length + 1; // 1 - for year row
            for (int row = startRow; row < this.Rows; row += numRowsInSection)
            {
                string errHDR = "Emission Health Impacts (R:" + (row + 1).ToString("0000") + "):  ";
                // only parse valid model years
                int year = this.GetInt32(row, 0);
                if (years.Contains(year))
                {
                    this.LogError(errHDR + "Duplicate calendar years are not allowed.");
                }
                else if (year >= 1900 && year <= 2200)
                {
                    years.Add(year);

                    for (int j = 0; j < HealthImpacts.List.Length; ++j)
                    {
                        int impactRow = j + row + 1;
                        var value = this.ParseCriteriaEmissionsValuesH(impactRow, 1);
                        values[HealthImpacts.List[j]].Add(value);
                    }
                }
            }

            // convert to value-by-year list
            int[] allYears = years.ToArray();
            var healthImpacts = new List<ValueByYear<CriteriaEmissionsValues>>();

            foreach (var ehi in HealthImpacts.List)
            {
                healthImpacts.Add(new ValueByYear<CriteriaEmissionsValues>(allYears, values[ehi].ToArray()));
            }

            return new EmissionHealthImpacts(healthImpacts);
        }
        GreenhouseEmissionCosts ParseGreenhouseEmissionCosts(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            var costsLowCO2     = new List<double>();
            var costsAvgCO2     = new List<double>();
            var costsHighCO2    = new List<double>();
            var costsVHighCO2   = new List<double>();

            var costsLowCH4     = new List<double>();
            var costsAvgCH4     = new List<double>();
            var costsHighCH4    = new List<double>();
            var costsVHighCH4   = new List<double>();

            var costsLowN2O     = new List<double>();
            var costsAvgN2O     = new List<double>();
            var costsHighN2O    = new List<double>();
            var costsVHighN2O   = new List<double>();

            int minCY = 0, maxCY = 0, prevYear = -1;
            //
            for (int row = 4; row < this.Rows; row++)
            {
                string errHDR = "Greenhouse Emission Costs (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                if (!ModelYear.IsValid(year)) { this.LogError(errHDR + "Calendar year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Calendar years are not contiguous."); }
                prevYear = year;
                //
                if (minCY == 0) { minCY = year; }
                else { maxCY = year; }
                costsLowCO2.Add(this.GetDouble(row, 1));
                costsAvgCO2.Add(this.GetDouble(row, 2));
                costsHighCO2.Add(this.GetDouble(row, 3));
                costsVHighCO2.Add(this.GetDouble(row, 4));

                costsLowCH4.Add(this.GetDouble(row, 5));
                costsAvgCH4.Add(this.GetDouble(row, 6));
                costsHighCH4.Add(this.GetDouble(row, 7));
                costsVHighCH4.Add(this.GetDouble(row, 8));

                costsLowN2O.Add(this.GetDouble(row, 9));
                costsAvgN2O.Add(this.GetDouble(row, 10));
                costsHighN2O.Add(this.GetDouble(row, 11));
                costsVHighN2O.Add(this.GetDouble(row, 12));
            }

            return new GreenhouseEmissionCosts(new Dictionary<string, object>
            {
                { "MinCY"           , minCY                     },
                { "MaxCY"           , maxCY                     },
                { "CostsLowCO2"     , costsLowCO2.ToArray()     },
                { "CostsAvgCO2"     , costsAvgCO2.ToArray()     },
                { "CostsHighCO2"    , costsHighCO2.ToArray()    },
                { "CostsVeryHighCO2", costsVHighCO2.ToArray()   },
                { "CostsLowCH4"     , costsLowCH4.ToArray()     },
                { "CostsAvgCH4"     , costsAvgCH4.ToArray()     },
                { "CostsHighCH4"    , costsHighCH4.ToArray()    },
                { "CostsVeryHighCH4", costsVHighCH4.ToArray()   },
                { "CostsLowN2O"     , costsLowN2O.ToArray()     },
                { "CostsAvgN2O"     , costsAvgN2O.ToArray()     },
                { "CostsHighN2O"    , costsHighN2O.ToArray()    },
                { "CostsVeryHighN2O", costsVHighN2O.ToArray()   }
            });
        }
        CriteriaEmissionCosts ParseCriteriaEmissionCosts(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);
            string sheetName = this.SheetNames[sheetIndex];

            var discountKeys = (DiscountRate[])Enum.GetValues(typeof(DiscountRate));
            var criteriaEmissionCostValues = new Estimates[] { Estimates.Low, Estimates.High, Estimates.Average };

            var years = new Dictionary<DiscountRate, List<int>>();
            var values = new Dictionary<DiscountRate, Dictionary<Estimates, List<CriteriaEmissionsValues>>>();
            foreach (var dk in discountKeys)
            {
                years.Add(dk, new List<int>());
                var dkDictionary = new Dictionary<Estimates, List<CriteriaEmissionsValues>>();
                foreach (var cecv in criteriaEmissionCostValues)
                {
                    dkDictionary.Add(cecv, new List<CriteriaEmissionsValues>());
                }
                values.Add(dk, dkDictionary);
            }

            Dictionary<Estimates, List<CriteriaEmissionsValues>> currentRate = null;
            List<int> currentYears = null;
            int startRow = 4;
            int numRowsInSection = criteriaEmissionCostValues.Length + 1; // 1 - for year row
            for (int row = startRow; row < this.Rows; row += numRowsInSection)
            {
                string errHDR = sheetName + " (R:" + (row + 1).ToString("0000") + "):  ";
                string rate = this.GetString(row, 0);
                if (rate.EndsWith("Discount Rate", true, null))
                {
                    var dkrate = (DiscountRate)char.GetNumericValue(rate[0]);
                    currentRate = values[dkrate];
                    currentYears = years[dkrate];
                    ++row;
                }
                // only parse valid model years
                int year = this.GetInt32(row, 0);
                if (currentYears.Contains(year))
                {
                    this.LogError(errHDR + "Duplicate calendar years are not allowed.");
                }
                else if (year >= 1900 && year <= 2200)
                {
                    currentYears.Add(year);

                    for (int j = 0; j < criteriaEmissionCostValues.Length; ++j)
                    {
                        int costRow = j + row + 1;
                        var value = this.ParseCriteriaEmissionsValuesH(costRow, 1);
                        currentRate[criteriaEmissionCostValues[j]].Add(value);
                    }
                }
            }

            // convert to value-by-year list
            var dr3 = new Dictionary<Estimates, ValueByYear<CriteriaEmissionsValues>>();
            foreach (var cecv in criteriaEmissionCostValues)
            {
                dr3.Add(cecv, new ValueByYear<CriteriaEmissionsValues>(years[DiscountRate.DR3].ToArray(),
                    values[DiscountRate.DR3][cecv].ToArray()));
            }
            var dr7 = new Dictionary<Estimates, ValueByYear<CriteriaEmissionsValues>>();
            foreach (var cecv in criteriaEmissionCostValues)
            {
                dr7.Add(cecv, new ValueByYear<CriteriaEmissionsValues>(years[DiscountRate.DR7].ToArray(),
                    values[DiscountRate.DR7][cecv].ToArray()));
            }

            return new CriteriaEmissionCosts(dr3, dr7);
        }
        UpstreamEmissions ParseUpstreamEmissions(int[] sheetIndexes, string[] sheetNames)
        {
            var emissions = new FTValue<ValueByYear<UpstreamEmissionsValue>>();
            for (int i = 0; i < sheetIndexes.Length; i++)
            {
                emissions.Items[i] = this.ParseUpstreamEmissions(sheetNames[i], sheetIndexes[i]);
            }
            return new UpstreamEmissions(emissions);
        }
        ValueByYear<UpstreamEmissionsValue> ParseUpstreamEmissions(string sheetName, int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            var years  = new List<int>();
            var values = new List<UpstreamEmissionsValue>();

            for (int row = 3; row < this.Rows;)
            {
                string errHDR = sheetName + " (R:" + (row + 1).ToString("0000") + "):  ";
                int step = 1;
                int year = this.GetInt32(row, 0);

                // only parse valid model years
                if (years.Contains(year))
                {
                    this.LogError(errHDR + "Duplicate calendar years are not allowed.");
                }
                else if (year >= 1900 && year <= 2200)
                {
                    years.Add(year);
                    step = 1;

                    EmissionsValue total = this.ParseEmissionsValueH(row, 1);
                    EmissionsValue extraction = null, transportation = null, refining = null, fuelTSD = null;

                    for (int yearRow = row + 1; yearRow < this.Rows; ++yearRow)
                    {
                        string category = this.GetString(yearRow, 0);

                        if (category.Equals("Petroleum Extraction", StringComparison.InvariantCultureIgnoreCase))
                        {
                            extraction = this.ParseEmissionsValueH(yearRow, 1);
                            step++;
                        }
                        else if (category.Equals("Petroleum Transportation", StringComparison.InvariantCultureIgnoreCase))
                        {
                            transportation = this.ParseEmissionsValueH(yearRow, 1);
                            step++;
                        }
                        else if (category.Equals("Petroleum Refining", StringComparison.InvariantCultureIgnoreCase))
                        {
                            refining = this.ParseEmissionsValueH(yearRow, 1);
                            step++;
                        }
                        else if (category.Equals("Fuel TS&D", StringComparison.InvariantCultureIgnoreCase))
                        {
                            fuelTSD = this.ParseEmissionsValueH(yearRow, 1);
                            step++;
                        }
                        else
                        {
                            break;
                        }
                    }

                    if (step == 1 || step == 5)
                    {
                        var upstreamValue = new UpstreamEmissionsValue();
                        for (int i = 0; i < upstreamValue.Items.Length; i++)
                        {
                            upstreamValue.Items[i] = new PetroleumCategoryValue(
                                (extraction == null) ? 0 : extraction.Items[i],
                                (transportation == null) ? 0 : transportation.Items[i],
                                (refining == null) ? 0 : refining.Items[i],
                                (fuelTSD == null) ? 0 : fuelTSD.Items[i],
                                (total == null) ? 0 : total.Items[i]);
                        }
                        values.Add(upstreamValue);
                    }
                    else
                    {
                        this.LogError(errHDR + "Incorrect number of petroleum production categories specified.");
                    }
                }

                row += step;
            }

            return new ValueByYear<UpstreamEmissionsValue>(
                years.ToArray(), values.ToArray(), UpstreamEmissions.InterpolateEmissionsValue);
        }
        TailpipeEmissions ParseTailpipeEmissions(int sheetIndex, string sheetName)
        {
            this.ActivateWorksheet(sheetIndex);

            // find min and max model years
            int minMY = -1, maxMY = -1;
            int prevYear = -1;
            int ageCounter = 0;

            for (int row = 4; row < this.Rows; row++)
            {
                string errHDR = sheetName + " (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                int age  = this.GetInt32(row, 1);
                if (year < 1900 || year > 2200) { this.LogError(errHDR + "Model year is not valid."); }
                if (age != (ageCounter++)) { this.LogError(errHDR + "Vehicle ages are not contiguous."); }
                if (ageCounter == 40) { ageCounter = 0; }
                if (ageCounter == 1 && prevYear != -1 && prevYear + 1 != year)
                {   // check for new model year at first age
                    if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Model years are not contiguous."); }
                }
                else if (ageCounter > 1 && prevYear != -1 && prevYear != year)
                {   // model year shouldn't change until vehicle age resets to zero
                    this.LogError(errHDR + "Model years are out of order.");
                }
                prevYear = year;
                //
                if (minMY == -1) { minMY = year; }
                else             { maxMY = year; }
            }

            // do not attempt to load input data if there were errors
            if (this.HasErrors) { return null; }

            // initialize emissions data
            VCValue<EmissionsValue[][]> emissions = new VCValue<EmissionsValue[][]>();
            int yearCount = maxMY - minMY + 1;
            for (int vc = 0; vc < emissions.Items.Length; vc++)
            {
                emissions.Items[vc] = new EmissionsValue[yearCount][];
                for (int yr = 0; yr < yearCount; yr++)
                {
                    emissions.Items[vc][yr] = new EmissionsValue[Globals.CY];
                    for (int age = 0; age < Globals.CY; age++)
                    {
                        emissions.Items[vc][yr][age] = new EmissionsValue();
                    }
                }
            }

            // load emissions data
            var partialPollutants = Pollutants.List.Where(x => x != Pollutant.CO2 && x != Pollutant.SO2);
            for (int yr = 0; yr < yearCount; yr++)
            {
                for (int age = 0; age < Globals.CY; age++)
                {
                    int col = 2;
                    foreach (Pollutant pollutant in partialPollutants)
                    {
                        for (int vc = 0; vc < emissions.Items.Length; vc++)
                        {
                            emissions.Items[vc][yr][age][pollutant] = this.GetDouble(4 + yr * Globals.CY + age, col++);
                        }
                    }
                }
            }

            return new TailpipeEmissions(new Dictionary<string, object>
            {
                { "MinMY", minMY },
                { "MaxMY", maxMY },
                { "Emissions", emissions }
            });
        }
        BrakeTireWearEmissions ParseBrakeTireWearEmissions(int sheetIndex)
        {
            this.ActivateWorksheet(sheetIndex);

            // Validate model years
            ValidateMYs(4, 2010);
            ValidateMYs(10, 2011);

            var emissions2010 = GetEmissions(4);
            var emissions2011 = GetEmissions(10);

            return new BrakeTireWearEmissions(new Dictionary<string, object> {
                ["Emissions2010"] = emissions2010,
                ["Emissions2011"] = emissions2011
            });

            void ValidateMYs(int row, int year)
            {
                for (int i = 0; i < FuelTypes.List.Length; i++) {
                    int my = this.GetInt32(row + i, 0);
                    if (my != year) {
                        this.LogError($"BTW_Emissions (R: { row + i}):  Model year {my} is not valid.");
                    }
                }
            }

            VCValue<FTValue<BrakeTireWearEmissions.BtwRates>> GetEmissions(int row)
            {
                var emissions = new VCValue<FTValue<BrakeTireWearEmissions.BtwRates>>();

                for (int i = 0; i < VehicleClasses.AltList.Length; i++) {
                    emissions.Items[i] = new FTValue<BrakeTireWearEmissions.BtwRates>();

                    for (int j = 0; j < FuelTypes.List.Length; j++) {
                        double breakWear = this.GetDouble(row + j, 2 + i);
                        double tireWear  = this.GetDouble(row + j, 5 + i);

                        emissions.Items[i].Items[j] = new BrakeTireWearEmissions.BtwRates(breakWear, tireWear);
                    }
                }

                return emissions;
            }
        }

        #region /* Helper methods */

        void ValidateModelYears(string sheetName, int sRow, out int minMY, out int maxMY, out int yearCount)
        {
            minMY = 0; maxMY = 0;
            int prevYear = -1;
            for (int row = sRow; row < this.Rows; row++)
            {
                string errHDR = sheetName + " (R:" + (row + 1).ToString("0000") + "):  ";
                if (this.GetString(row, 0).Length == 0) { continue; }
                int year = this.GetInt32(row, 0);
                if (year < 1900) { this.LogError(errHDR + "Model year is not valid."); }
                if (prevYear != -1 && prevYear + 1 != year) { this.LogError(errHDR + "Model years are not contiguous."); }
                prevYear = year;
                //
                if (minMY == 0) { minMY = year; }
                else            { maxMY = year; }
            }
            //
            yearCount = maxMY - minMY + 1;
        }

        double[] GetDiscRates(int row, int column, string title)
        {
            double[] value = this.GetDoubleArr(row, column);
            value.Sort();
            // perform some error-checking
            for (int i = 0; i < value.Length; i++)
            {
                double d = value[i];
                if (double.IsNaN(d) || d <= 0)
                {
                    this.LogError("One or more " + title + " discount rates is not valid.");
                    break;
                }
                if (i > 0 && d == value[i - 1])
                {
                    this.LogError("Duplicate " + title + " discount rates are not allowed.");
                    break;
                }
            }
            return value;
        }
        double[] GetDoubleArr(int row, int column)
        {
            // format:
            //  123.456, 456.789, 789.123, ...
            //  123.456; 456.789; 789.123; ...
            string str = this.GetString(row, column);
            if (str.Length == 0) { return Array.Empty<double>(); }
            //
            string[] arr = str.Split(',', ';');
            double[] value = new double[arr.Length];
            for (int i = 0; i < arr.Length; i++)
            {
                value[i] = double.TryParse(arr[i], out double d) ? d : double.NaN;
            }
            return value;
        }

        FuelValue ParseFuelValue(int sRow, int column)
        {   //
            // scans column vertically starting at sRow
            // returns new FuelValue object
            // parameters file must contain all required fuel types in correct order
            //
            FuelValue value = new FuelValue();
            foreach (FuelType fuel in FuelTypes.List)
            {
                value[fuel] = this.GetDouble(sRow++, column);
            }
            return value;
        }
        FuelValue ParseFuelValueH(int row, int sColumn)
        {   //
            // scans row horizontally starting at sColumn
            // returns new FuelValue object
            // parameters file must contain all required fuel types in correct order
            //
            FuelValue value = new FuelValue();
            foreach (FuelType fuel in FuelTypes.List)
            {
                value[fuel] = this.GetDouble(row, sColumn++);
            }
            return value;
        }
        EmissionsValue ParseEmissionsValueH(int row, int sColumn)
        {   //
            // scans row horizontally starting at sColumn
            // returns new EmissionsValue object
            // parameters file must contain all supported pollutants in correct order
            //
            EmissionsValue value = new EmissionsValue();
            foreach (Pollutant pollutant in Pollutants.List)
            {
                value[pollutant] = this.GetDouble(row, sColumn++);
            }
            return value;
        }
        CriteriaEmissionsValues ParseCriteriaEmissionsValuesH(int row, int sColumn)
        {
            //
            // scans row horizontally starting at sColumn
            // returns new CriteriaEmissionsValue object
            // parameters file must contain all required criteria pollutants (upstream/tailpipe) in correct order
            //

            // local helper function
            CriteriaEmissionsValue ParseCriteriaEmissionsValueHelper()
            {
                return new CriteriaEmissionsValue(new Dictionary<string, object> {
                    ["NOX"] = this.GetDouble(row, sColumn++),
                    ["SO2"] = this.GetDouble(row, sColumn++),
                    ["PM" ] = this.GetDouble(row, sColumn++)
                });
            }

            var upstreamRefineries = ParseCriteriaEmissionsValueHelper();
            var upstreamExtraction = ParseCriteriaEmissionsValueHelper();
            var upstreamTransportation = ParseCriteriaEmissionsValueHelper();
            var upstreamFuelTSD = ParseCriteriaEmissionsValueHelper();
            var upstreamElectricity = ParseCriteriaEmissionsValueHelper();

            var downstreamGasLDV = ParseCriteriaEmissionsValueHelper();
            var downstreamGasLDT = ParseCriteriaEmissionsValueHelper();
            var downstreamDslLDx = ParseCriteriaEmissionsValueHelper();
            var downstreamGas2b3 = ParseCriteriaEmissionsValueHelper();
            var downstreamDsl2b3 = ParseCriteriaEmissionsValueHelper();

            return new CriteriaEmissionsValues(new Dictionary<string, object>
            {
                // upstream values
                ["UpstreamRefineries"    ] = upstreamRefineries,
                ["UpstreamExtraction"    ] = upstreamExtraction,
                ["UpstreamTransportation"] = upstreamTransportation,
                ["UpstreamFuelTSD"       ] = upstreamFuelTSD,
                ["UpstreamElectricity"   ] = upstreamElectricity,

                // downstream values
                ["DownstreamGasLDV"] = downstreamGasLDV,
                ["DownstreamGasLDT"] = downstreamGasLDT,
                ["DownstreamDslLDx"] = downstreamDslLDx,
                ["DownstreamGas2b3"] = downstreamGas2b3,
                ["DownstreamDsl2b3"] = downstreamDsl2b3
            });
        }
        EVRechargeThresholds ParseEVThresholds(int sRow, int column)
        {   //
            // scans column vertically starting at sRow
            // returns new EVRechargeThresholds object
            // parameters file must contain all required values in correct order
            //
            return new EVRechargeThresholds(
                new Dictionary<string, object>
                {
                    { "ChargeFrequency",    this.GetDouble(sRow++, column) },
                    { "ShareCharged",       this.GetDouble(sRow++, column) },
                    { "ChargeRate",         this.GetDouble(sRow++, column) }
                });
        }

        #endregion

        #endregion

        #region /*** Variables ***/

        internal Parameters _value;

        #endregion

    }
}
