#region << Using Directives >>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Volpe.Cafe.Generic;
using Volpe.Cafe.MarketData;
using Volpe.Cafe.MarketData.BaselineInfo;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.MarketData.Parsers;
using Volpe.Cafe.Utils;

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

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

        private struct YearRange
        {
            public int MinYear { get; set; }
            public int MaxYear { get; set; }
            public int YearCount => this.MaxYear - this.MinYear + 1;

            public static implicit operator YearRange((int MinYear, int MaxYear) value)
            {
                return new YearRange { MinYear = value.MinYear, MaxYear = value.MaxYear };
            }

            public void Deconstruct(out int minYear, out int maxYear)
            {
                minYear = this.MinYear;
                maxYear = this.MaxYear;
            }
        }

        #region /*** Ctors ***/

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

        #endregion

        #region /*** Methods ***/

        protected override void ParseInternal()
        {
            this._ind = null;

            // scan each worksheet name, looking for a match
            string[] sheets = this.SheetNames;
            string[] names  = { "Manufacturers", "Credits and Adjustments",
                                "Vehicles", "Platforms", "Engines", "Transmissions" };
            int[] indexes = Enumerable.Repeat(-1, names.Length).ToArray();

            for (int i = 0; i < sheets.Length; i++) {
                for (int j = 0; j < names.Length; j++) {
                    if (StringHelper.Equals(sheets[i], names[j])) {
                        indexes[j] = i;
                        break;
                    }
                }
            }
            if (!this.VerifyIndexes("Market Data workbook", indexes, names)) {
                return;
            }

            // parse data, starting with mfrs sheet --
            // if mfrs sheet parsed without errors, we can used the resulting mfr list
            // as reference for validating all other sheets
            var bMfrsAndBank = this.LoadManufacturers(indexes[0]);
            if (this.HasErrors) { return; }

            var bMfrs = bMfrsAndBank.ConvertAll(item => item.Item1);
            var mfrNames = bMfrs.ConvertAll(item => item.Name);

            var bTrnsD = this.LoadTransmissions(indexes[5], mfrNames);
            var bEngsD = this.LoadEngines      (indexes[4], mfrNames);
            var bPltsD = this.LoadPlatforms    (indexes[3], mfrNames);
            var bVehs  = this.LoadVehicles     (indexes[2], mfrNames, bMfrs, bPltsD, bEngsD, bTrnsD);
            var bCrAdj = this.LoadCreditsAndAdjustments(indexes[1], mfrNames);

            // finalize component baseline info
            this.ParseComponentRefreshRedesignYears(bVehs, bTrnsD, "Transmission", veh => veh.TransmissionCode);
            this.ParseComponentRefreshRedesignYears(bVehs, bEngsD, "Engine", veh => veh.EngineCode);
            this.ParseComponentRefreshRedesignYears(bVehs, bPltsD, "Platform", veh => veh.PlatformCode);

            var bTrns = bTrnsD.ConvertAll(item => new BaselineTransmission(item));
            var bEngs = bEngsD.ConvertAll(item => new BaselineEngine(item));
            var bPlts = bPltsD.ConvertAll(item => new BaselinePlatform(item));

            // load families from their base components
            var bPltFams = InitBaselineComponentFamily(bPlts, x => x.FamilyName);
            var bEngFams = InitBaselineComponentFamily(bEngs, x => x.FamilyName);
            var bTrnFams = InitBaselineComponentFamily(bTrns, x => x.FamilyName);

            List<BaselineComponentFamily> InitBaselineComponentFamily<T>(List<T> bInfos, Func<T, string> getFamily)
                where T : IOEMProductInfo
            {
                var familiesSet = new HashSet<(string Mfr, string Family)>();
                foreach (var bInfo in bInfos) {
                    familiesSet.Add((bInfo.ManufacturerName, getFamily(bInfo)));
                }
                var familiesList = familiesSet.ToList();

                var bFams = new List<BaselineComponentFamily>();
                for (int i = 0; i < familiesList.Count; i++) {
                    var values = new Dictionary<string, object> {
                        ["Code"] = 1 + i,
                        ["ManufacturerName"] = familiesList[i].Mfr,
                        ["FamilyName"] = familiesList[i].Family
                    };
                    var baselineInfo = new BaselineComponentFamily(values);
                    bFams.Add(baselineInfo);
                }
                return bFams;
            }

            // early termination if errors were encountered
            if (this.HasErrors) { return; }

            // ensure all product codes are unique across entire industry
            this.ValidateUniqueProductCodes(bVehs, "vehicle");
            this.ValidateUniqueProductCodes(bPlts, "platform");
            this.ValidateUniqueProductCodes(bEngs, "engine");
            this.ValidateUniqueProductCodes(bTrns, "transmission");
            this.ValidateUniqueProductCodes(bPltFams, "platform family");
            this.ValidateUniqueProductCodes(bEngFams, "engine family");
            this.ValidateUniqueProductCodes(bTrnFams, "transmission family");

            // ensure component family names and platform names are unique across entire industry
            // note: because of the way component families are currently initialized, there won't
            //       be duplicates (i.e., uniqueness is guaranteed); but the below is a safety
            //       check in case family initialization changes
            this.ValidateUniqueProductNames(bPltFams, x => x.FamilyName, "platform family");
            this.ValidateUniqueProductNames(bEngFams, x => x.FamilyName, "engine family");
            this.ValidateUniqueProductNames(bTrnFams, x => x.FamilyName, "transmission family");
            this.ValidateUniqueProductNames(bPlts, x => x.Name, "platform");

            if (this.HasErrors) {
                return;
            }

            // init industry
            try {
                var bMfrsAll = Enumerable.Zip(bMfrsAndBank, bCrAdj, (x, y) => (x.Item1, x.Item2, y)).ToList();
                this._ind = new Industry(bMfrsAll, bVehs, bPlts, bEngs, bTrns, bPltFams, bEngFams, bTrnFams);
            }
            catch (Exception ex) {
                this.LogError(ex.Message);
            }
        }

        void ValidateUniqueProductCodes<T>(List<T> items, string productText) where T : IOEMProductInfo
        {
            var uniqueCodes = new HashSet<int>();

            foreach (var item in items)
            {
                int code = item.Code;
                if (uniqueCodes.Contains(code))
                {
                    this.LogError($"Duplicate {productText} code detected: {code} " +
                        $"({productText} codes must be unique across the entire industry).");
                }
                else
                {
                    uniqueCodes.Add(code);
                }
            }
        }
        void ValidateUniqueProductNames<T>(List<T> items, Func<T, string> getName, string productText) where T : IOEMProductInfo
        {
            var uniqueCodes = new HashSet<string>();

            foreach (var item in items) {
                string name = getName(item);
                if (uniqueCodes.Contains(name)) {
                    this.LogError($"Duplicate {productText} name detected: {name} " +
                        $"({productText} names must be unique across the entire industry).");
                }
                else {
                    uniqueCodes.Add(name);
                }
            }
        }

        // ----- Methods for parsing the manufacturers worksheet -----
        List<(BaselineManufacturer, BaselineCreditBank)> LoadManufacturers(int index)
        {
            this.ActivateWorksheet(index);

            // ----- detect column indexes -----
            int rows = this.Rows;
            int cols = this.Columns;
            string[] namesMain = {
                "Manufacturer Code",        // 0
                "Manufacturer Name",        // 1
                "Prefer Fines",             // 2 (section title; multiple model years)
                "Payback Period",           // 3 (section title; veh-style cols)
                "CA+S177 Sales (%)",        // 4 (multiple by-year columns)
                "Ignore ZEV PHEV Cap",      // 5
                "CA+S177 HDPUV Sales (%)",  // 6
                "CARB Agreement",           // 7
                "EPA Multiplier Mode",      // 8 (section title; alt-reg-class cols)
            };

            // scan each column to find the appropriate column names
            int[] indexesMain = this.DetectColumnIndexes(cols, namesMain);

            // above searches column names only (at row 1)
            // update indexes to search for section headers (at row 0)
            int preferFinesIndex = Array.IndexOf(namesMain, "Prefer Fines");
            YearRange preferFinesRange = new YearRange();
            int paybackIndex = Array.IndexOf(namesMain, "Payback Period");
            int epaModeIndex = Array.IndexOf(namesMain, "EPA Multiplier Mode");

            // for ZEV sales, we want to match multiple cols (starting with specified prefix)
            int zevSalesShareIndex = Array.IndexOf(namesMain, "CA+S177 Sales (%)");
            YearRange zevSalesShareRange = new YearRange();

            for (int i = 0; i < cols; i++)
            {
                string section = this.GetString(0, i);
                string column = this.GetString(1, i);

                if (StringHelper.Equals(section, namesMain[preferFinesIndex], fuzzy: true))
                {
                    indexesMain[preferFinesIndex] = i;
                    preferFinesRange = this.VerifyPreferFinesSectionHeaders(
                        this.SheetNames[index], cols, i, namesMain[preferFinesIndex]);
                    i += preferFinesRange.YearCount - 1;
                }
                else if (StringHelper.Equals(section, namesMain[paybackIndex], fuzzy: true))
                {
                    indexesMain[paybackIndex] = i;

                    // payback period section defines columns based on "generic" vehicle styles
                    // in the input, they **must be** defined in the same order as "VS.AltNames"
                    this.VerifyPaybackOrEPAModeSectionHeaders(
                        this.SheetNames[index], cols, i, namesMain[paybackIndex], VehicleStyles.AltNames);
                }
                else if (StringHelper.Equals(section, namesMain[epaModeIndex], fuzzy: true))
                {
                    indexesMain[epaModeIndex] = i;

                    // EPA multiplier mode section defines columns based on alt reg-classes
                    // in the input, they **must be** defined in the same order as "RC.AltNames"
                    this.VerifyPaybackOrEPAModeSectionHeaders(
                        this.SheetNames[index], cols, i, namesMain[epaModeIndex], RegClasses.AltNames);
                }
                else if (StringHelper.StartsWith(column, namesMain[zevSalesShareIndex], fuzzy: true))
                {
                    indexesMain[zevSalesShareIndex] = i;
                    zevSalesShareRange = this.VerifyZevSalesShareColumnHeaders(
                        this.SheetNames[index], cols, i, namesMain[zevSalesShareIndex]);
                    i += zevSalesShareRange.YearCount - 1;
                }
            }

            // check if all required columns were found
            if (!this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesMain, namesMain))
            {
                return null;
            }

            // look for credit banks
            var bcHeaders = new Dictionary<string, Dictionary<int, int>>
            {
                { "DC-BANK-" , new Dictionary<int, int>() },
                { "IC-BANK-" , new Dictionary<int, int>() },
                { "LT-BANK-" , new Dictionary<int, int>() },
                { "HDPUV-BANK-", new Dictionary<int, int>() }
            };
            var co2bcHeaders = new Dictionary<string, Dictionary<int, int>>
            {
                { "PC-CO2-BANK-" , new Dictionary<int, int>() },
                { "LT-CO2-BANK-" , new Dictionary<int, int>() },
                { "HDPUV-CO2-BANK-", new Dictionary<int, int>() }
            };

            for (int i = 0; i < cols; i++)
            {
                string rowData = this.GetString(1, i);
                bool foundCol = false;

                this.ParseBankedCreditHeaders(bcHeaders, rowData, i, ref foundCol);
                this.ParseBankedCreditHeaders(co2bcHeaders, rowData, i, ref foundCol);
            }

            // check if all required credit bank columns were found
            if (!this.ValidateBankedCredits(bcHeaders, co2bcHeaders))
            {
                return null;
            }

            // if any other errors were logged during initial parsing, do not load any data
            if (this.HasErrors)
            {
                return null;
            }

            // ----- load the manufacturer data -----
            var bMfr = new List<(BaselineManufacturer, BaselineCreditBank)>();
            for (int i = 2; i < rows; i++)
            {
                BaselineManufacturer baselineInfo =
                    this.LoadManufacturer(i, indexesMain, preferFinesRange, zevSalesShareRange);
                BaselineCreditBank baselineCreditBank =
                    this.LoadManufacturerCreditBanks(i, indexesMain, bcHeaders, co2bcHeaders);

                if (baselineInfo != null && baselineCreditBank != null)
                {
                    bMfr.Add((baselineInfo, baselineCreditBank));
                }
            }

            return bMfr;
        }

        YearRange VerifyPreferFinesSectionHeaders(string wsName, int colCount, int col, string colName)
        {
            // prefer fines section defines columns for multiple model years
            // the model years must be sequential and within the min/max year range
            // defined in ModelYearKey
            int minYear = -1;
            int maxYear = -1;

            for (int j = col; j < colCount; j++)
            {
                if (j != col && this.GetString(0, j) != string.Empty)
                {
                    break; // found new section
                }

                string yearStr = this.GetString(1, j);
                if (!yearStr.IsNumeric())
                {
                    this.LogError($"In the {wsName} worksheet, {colName} section contains a " +
                                  $"model year value '{yearStr}', which is not numeric.");
                    continue;
                }

                int year = yearStr.GetInt32();
                if (!ModelYearKey.IsValid(year))
                {
                    this.LogError($"In the {wsName} worksheet, {colName} section contains a " +
                                  $"model year value '{year}', which is outside the valid range " +
                                  $"of years ({ModelYearKey.MinYear} - {ModelYearKey.MaxYear}).");
                    continue;
                }

                if (minYear == -1)
                {
                    minYear = year;
                }
                if (maxYear != -1 && maxYear + 1 != year)
                {
                    this.LogError($"In the {wsName} worksheet, model years within the " +
                                  $"{colName} section are not sequential.");
                    break;
                }
                maxYear = year;
            }

            return (minYear, maxYear);
        }

        YearRange VerifyZevSalesShareColumnHeaders(string wsName, int colCount, int col, string colName)
        {
            // ZEV sales share columns are defined for multiple model years
            // the model years must be sequential and within the min/max year range
            // defined in ModelYearKey
            int minYear = -1;
            int maxYear = -1;

            for (int j = col; j < colCount; j++) {
                string column = this.GetString(1, j);
                if (j != col && !StringHelper.StartsWith(column, colName, fuzzy: true)) {
                    break; // found unrelated column
                }

                string yearStr = column.Substring(colName.Length).Trim('-', '_', ' ');
                if (!yearStr.IsNumeric()) {
                    this.LogError($"In the {wsName} worksheet, {colName} section contains a " +
                                  $"model year value '{yearStr}', which is not numeric.");
                    continue;
                }

                int year = yearStr.GetInt32();
                if (!ModelYearKey.IsValid(year)) {
                    this.LogError($"In the {wsName} worksheet, {colName} section contains a " +
                                  $"model year value '{year}', which is outside the valid range " +
                                  $"of years ({ModelYearKey.MinYear} - {ModelYearKey.MaxYear}).");
                    continue;
                }

                if (minYear == -1) {
                    minYear = year;
                }
                if (maxYear != -1 && maxYear + 1 != year) {
                    this.LogError($"In the {wsName} worksheet, model years within the " +
                                  $"{colName} section are not sequential.");
                    break;
                }
                maxYear = year;
            }

            return (minYear, maxYear);
        }

        void VerifyPaybackOrEPAModeSectionHeaders(
            string wsName, int colCount, int col, string colName, string[] validNames)
        {
            // payback period section defines columns based on "generic" vehicle styles
            // EPA multiplier mode section defines columns based on alt reg-classes
            // in the input, they **must be** defined in the same order as validNames
            for (int j = 0; j < validNames.Length; j++)
            {
                string column = this.GetString(1, col + j);
                if (!StringHelper.Equals(column, validNames[j], fuzzy: true))
                {
                    this.LogError($"In the {wsName} worksheet, {colName} section is not set up correctly.");
                    break;
                }
            }
        }

        BaselineManufacturer LoadManufacturer(
            int row, int[] indexesMain, YearRange preferFinesRange, YearRange zevSalesShareRange)
        {
            //----- load data -----
            string mfrCode            = this.GetString(row, indexesMain[0]);
            string mfrName            = this.GetString(row, indexesMain[1]);
            var    preferFines        = Enumerable.Range(indexesMain[2], preferFinesRange.YearCount)
                                                  .Select(column => this.GetString(row, column)).ToList();
            var    paybackPeriod      = Enumerable.Range(indexesMain[3], VehicleStyles.AltNames.Length)
                                                  .Select(column => this.GetData(row, column)).ToList();
            var    zevSalesShare      = Enumerable.Range(indexesMain[4], zevSalesShareRange.YearCount)
                                                  .Select(column => this.GetData(row, column)).ToList();
            string ignoreZevPhevCap   = this.GetString(row, indexesMain[5]);
            object zevSalesShareHDPUV = this.GetData  (row, indexesMain[6]);
            string carbAgreement      = this.GetString(row, indexesMain[7]);
            var    epaMode            = Enumerable.Range(indexesMain[8], RegClasses.AltNames.Length)
                                                  .Select(column => this.GetData(row, column)).ToList();

            // if mfr code is X-ed out, assume user wants to ignore mfr from analysis
            // if mfr code and name are blank, assume the row is empty
            if (mfrCode == "X" || (string.IsNullOrEmpty(mfrCode) && string.IsNullOrEmpty(mfrName)))
            {
                return null;
            }

            //----- parse data -----
            char[] delim = { ';', ',' };
            var parsedPreferFines = new RCValue<bool[]>(() => new bool[preferFinesRange.YearCount]);
            for (int yr = 0; yr < preferFinesRange.YearCount; yr++)
            {
                // prefer fines format:
                //  option (1) a cell can be a single value ("Y", "YES", "NO", "FALSE", ...), 
                //  option (2) a cell can have multiple delimited values for each reg-class (e.g., "Y;N;NO;TRUE")
                //  option (3) a cell can be blank, in which case we assume mfr prefers not to pay fines for any RC
                string[] pfCellItems = preferFines[yr].Split(delim);
                if (pfCellItems.Length == 1 || pfCellItems.Length == parsedPreferFines.Items.Length)
                {
                    for (int rc = 0; rc < parsedPreferFines.Items.Length; rc++)
                    {
                        int arrIndex = (pfCellItems.Length == 1) ? 0 : rc;
                        parsedPreferFines.Items[rc][yr] = StringHelper.EqualsAny(pfCellItems[arrIndex], TrueStrings);
                    }
                }
            }

            var parsedPaybackPeriod = new VSValue<double>();
            for (int i = 0; i < paybackPeriod.Count; i++)
            {
                parsedPaybackPeriod.Items[i] = paybackPeriod[i].GetDouble();
            }

            var parsedZevSalesShare = zevSalesShare.ConvertAll(item => item.GetDouble());
            double parsedZevSalesShareHDPUV = zevSalesShareHDPUV.GetDouble();
            bool parsedIgnoreZevPhevCap = StringHelper.EqualsAny(ignoreZevPhevCap, TrueStrings);
            bool parsedCarbAgreement = StringHelper.EqualsAny(carbAgreement, TrueStrings);

            var parsedEpaMode = new RCAltValue<int>();
            for (int i = 0; i < epaMode.Count; i++)
            {
                parsedEpaMode.Items[i] = epaMode[i].GetInt32();
            }

            //----- validate data -----
            // other than mfr code and name, all columns are optional
            // however, if they are specified, they must be valid
            string hdr = "Manufacturer (R:" + (row + 1).ToString("0000") + "):  ";

            if (!mfrCode.IsInteger()) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Manufacturer Code"); }
            if (mfrName.Length == 0) { this.LogError(hdr + "Brand is not specified."); }

            for (int i = 0; i < preferFines.Count; i++)
            {
                // prefer fines may have multiple formats as defined above
                string value = preferFines[i];
                if (value.Length == 0)
                    continue;

                string[] pfCellItems = value.Split(delim);
                if (pfCellItems.Length != 1 && pfCellItems.Length != RegClasses.List.Length)
                {
                    this.LogError($"{hdr}Prefer Fines (MY-{preferFinesRange.MinYear + i}) is not formatted properly.");
                }

                bool pfNotValid = false;
                foreach (var pfCellItem in pfCellItems)
                {
                    pfNotValid |= !StringHelper.EqualsAny(pfCellItem, TrueStrings)
                               && !StringHelper.EqualsAny(pfCellItem, FalseStrings);
                }
                if (pfNotValid)
                {
                    this.LogError($"{hdr}Prefer Fines (MY-{preferFinesRange.MinYear + i}) is not valid.");
                }
            }

            for (int i = 0; i < paybackPeriod.Count; i++)
            {
                string value = paybackPeriod[i].ToString().Trim();
                double parsedValue = parsedPaybackPeriod.Items[i];
                if (value.Length > 0 && (!value.IsNumeric() || parsedValue < 0))
                {
                    this.LogError($"{hdr}Payback Period ({VehicleStyles.AltNames[i]}) is not valid.");
                }
            }

            for (int i = 0; i < zevSalesShare.Count; i++)
            {
                string value = zevSalesShare[i].ToString().Trim();
                double parsedValue = parsedZevSalesShare[i];
                if (value.Length > 0 && (!value.IsNumeric() || parsedValue < 0 || parsedValue > 1))
                {
                    this.LogError($"{hdr}CA+S177 Sales (%) - {zevSalesShareRange.MinYear + i} is not valid.");
                }
            }

            if (zevSalesShareHDPUV.ToString().Trim().Length > 0
                && (!zevSalesShareHDPUV.IsNumeric() || parsedZevSalesShareHDPUV < 0 || parsedZevSalesShareHDPUV > 1))
            {
                this.LogError($"{hdr}CA+S177 HDPUV Sales (%) is not valid.");
            }
            if (ignoreZevPhevCap.Length > 0
                && !StringHelper.EqualsAny(ignoreZevPhevCap, TrueStrings)
                && !StringHelper.EqualsAny(ignoreZevPhevCap, FalseStrings))
            {
                this.LogError($"{hdr}Ignore ZEV PHEV Cap is not valid.");
            }

            if (carbAgreement.Length > 0
                && !StringHelper.EqualsAny(carbAgreement, TrueStrings)
                && !StringHelper.EqualsAny(carbAgreement, FalseStrings))
            {
                this.LogError($"{hdr}CARB Agreement is not valid.");
            }

            for (int i = 0; i < epaMode.Count; i++)
            {
                string value = epaMode[i].ToString().Trim();
                int parsedValue = parsedEpaMode.Items[i];
                if (value.Length > 0 && (!value.IsInteger() || parsedValue < 0 || parsedValue > 3))
                {
                    this.LogError($"{hdr}EPA Multiplier Mode ({RegClasses.AltNames[i]}) is not valid.");
                }
            }

            //----- init and return data -----
            var values = new Dictionary<string, object>
            {
                ["Code"] = mfrCode.GetInt32(),
                ["Name"] = mfrName,
                ["PreferFines"] = new BaselineManufacturer.PreferFinesValue(preferFinesRange.MinYear,
                                                                            preferFinesRange.MaxYear,
                                                                            parsedPreferFines),
                ["PaybackPeriod"] = new BaselineManufacturer.PaybackPeriodValue(parsedPaybackPeriod),
                ["ZevSalesShare"] = new ReadOnlyAnnualValue<double>(zevSalesShareRange.MinYear,
                                                                    zevSalesShareRange.MaxYear,
                                                                    values: parsedZevSalesShare,
                                                                    outOfBoundsMode: 1),
                ["ZevCreditShare"] = 1,
                ["IgnoreZevPhevCap"] = parsedIgnoreZevPhevCap,
                ["ZevSalesShareHDPUV"] = parsedZevSalesShareHDPUV,
                ["ZevCreditShareHDPUV"] = 1,
                ["CARBAgreement"] = parsedCarbAgreement,
                ["EPAMultiplierMode"] = new BaselineManufacturer.EPAMultiplierModeValue(parsedEpaMode)
            };
            var baselineInfo = new BaselineManufacturer(values);
            return baselineInfo;
        }

        BaselineCreditBank LoadManufacturerCreditBanks(
            int row, int[] indexesMain,
            Dictionary<string, Dictionary<int, int>> bcHeaders,
            Dictionary<string, Dictionary<int, int>> co2bcHeaders)
        {
            string mfrCode = this.GetString(row, indexesMain[0]);
            string mfrName = this.GetString(row, indexesMain[1]);

            // if mfr code is X-ed out, assume user wants to ignore mfr from analysis
            // if mfr code and name are blank, assume the row is empty
            if (mfrCode == "X" || (string.IsNullOrEmpty(mfrCode) && string.IsNullOrEmpty(mfrName)))
            {
                return null;
            }

            var sortedYears = bcHeaders.First().Value.Keys.OrderBy(k => k);
            int minYear = sortedYears.First();
            int maxYear = sortedYears.Last();
            int yearCount = maxYear - minYear + 1;

            // load CAFE Bank section
            var cafeBank = new RCValue<double[]>(() => new double[yearCount]);
            foreach (RegulatoryClass rc in RegClasses.List)
            {
                int index = RCValue<Dictionary<int, double>>.GetIndex(rc);
                string acronym = RegClasses.Acronyms[index];

                double[] rcItem = cafeBank.Items[index];
                Dictionary<int, int> headerColumns = bcHeaders.First(x => x.Key.StartsWith(acronym)).Value;
                foreach (KeyValuePair<int, int> headerColumn in headerColumns) // <year, column>
                {
                    rcItem[headerColumn.Key - minYear] = this.GetDouble(row, headerColumn.Value);
                }
            }

            // load CO-2 Bank section
            var co2Bank = new RCAltValue<double[]>(() => new double[yearCount]);
            foreach (RegulatoryClass rc in RegClasses.AltList)
            {
                int index = RCAltValue<Dictionary<int, double>>.GetIndex(rc);
                string acronym = RegClasses.AltAcronyms[index];

                double[] rcItem = co2Bank.Items[index];
                Dictionary<int, int> headerColumns = co2bcHeaders.First(x => x.Key.StartsWith(acronym)).Value;
                foreach (KeyValuePair<int, int> headerColumn in headerColumns) // <year, column>
                {
                    rcItem[headerColumn.Key - minYear] = this.GetDouble(row, headerColumn.Value);
                }
            }

            var baselineCreditBank = new BaselineCreditBank(minYear, maxYear, cafeBank, co2Bank);
            return baselineCreditBank;
        }

        void ParseBankedCreditHeaders(
            Dictionary<string, Dictionary<int, int>> headers, string rowData, int col, ref bool foundCol)
        {
            if (foundCol)
                return;

            foreach (var header in headers)
            {
                if (!rowData.StartsWith(header.Key))
                    continue;

                string yearStr = rowData.Substring(header.Key.Length);
                if (int.TryParse(yearStr, out int year) &&
                    header.Value.ContainsKey(year) == false)
                {
                    header.Value.Add(year, col);
                    foundCol = true;
                    break;
                }
            }
        }

        bool ValidateBankedCredits(
            Dictionary<string, Dictionary<int, int>> bcHeaders, Dictionary<string, Dictionary<int, int>> co2bcHeaders)
        {
            bool isValid = true;

            IOrderedEnumerable<int> sortedYears = bcHeaders.First().Value.Keys.OrderBy(k => k);
            if (sortedYears.Any() == false || sortedYears.First() < 2000)
            {
                isValid = false;
                this.LogError("In the Manufacturers worksheet, the Banked Credits years cannot be before 2000.");
            }
            else
            {
                var all = Enumerable.Range(sortedYears.First(), sortedYears.Last() - sortedYears.First() + 1);
                if (sortedYears.SequenceEqual(all) == false)
                {
                    isValid = false;
                    this.LogError("In the Manufacturers worksheet, the Banked Credits years are not sequential.");
                }
            }

            foreach (var bch in bcHeaders.Skip(1))
            {
                IOrderedEnumerable<int> currSortedYears = bch.Value.Keys.OrderBy(k => k);
                if (sortedYears.Count() != currSortedYears.Count() ||
                    sortedYears.SequenceEqual(currSortedYears) == false)
                {
                    isValid = false;
                    this.LogError("In the Manufacturers worksheet, the Banked Credits years have different range.");
                    break;
                }
            }
            foreach (var co2bch in co2bcHeaders)
            {
                IOrderedEnumerable<int> currSortedYears = co2bch.Value.Keys.OrderBy(k => k);
                if (sortedYears.Count() != currSortedYears.Count() ||
                    sortedYears.SequenceEqual(currSortedYears) == false)
                {
                    isValid = false;
                    this.LogError("In the Manufacturers worksheet, the Banked CO-2 Credits years have different range.");
                    break;
                }
            }

            return isValid;
        }

        // ----- Methods for parsing the vehicles worksheet -----
        List<BaselineVehicle> LoadVehicles(
            int index, List<string> mfrNames,
            List<BaselineManufacturer> bMfrs,
            List<Dictionary<string, object>> bPlts,
            List<Dictionary<string, object>> bEngs,
            List<Dictionary<string, object>> bTrns)
        {
            this.ActivateWorksheet(index);

            // ----- detect column indexes -----
            int rows = this.Rows;
            int cols = this.Columns;
            string[] namesMain = {
                "Vehicle Code",                 //  0
                "Manufacturer",                 //  1
                "Brand",                        //  2
                "Model",                        //  3
                "Nameplate",                    //  4
                "Platform Code",                //  5
                "Engine Code",                  //  6
                "Transmission Code",            //  7
                "Sales",                        //  8
                "MSRP",                         //  9
                "Regulatory Class",             // 10
                "Regulatory Class 2",           // 11
                "Technology Class",             // 12
                "Engine Technology Class",      // 13
                "Safety Class",                 // 14
                "ZEV Candidate",                // 15
                "Origin",                       // 16
                "Style",                        // 17
                "Drive",                        // 18
                "Footprint",                    // 19
                "Curb Weight (MR0)",            // 20
                "Curb Weight",                  // 21
                "GVWR",                         // 22
                "GCWR",                         // 23
                "Max GVWR/CW",                  // 24
                "Max GCWR/GVWR",                // 25
                "Fuel Capacity",                // 26
                "Vehicle Power",                // 27
                "Refresh Years",                // 28
                "Redesign Years",               // 29
                "Dealership Employment Hours",  // 30
                "US Assembly Employment Hours", // 31
                "Percent US Content",           // 32
                "ZEV Application Year",         // 33
                "ZEV Reference Vehicle"         // 34
            };
            string[] namesFuel = {
                // fuel economy
                "Fuel Economy (G)",     //  0
                "Fuel Economy (E85)",   //  1
                "Fuel Economy (D)",     //  2
                "Fuel Economy (E)",     //  3
                "Fuel Economy (H)",     //  4
                "Fuel Economy (CNG)",   //  5
                // fuel share
                "Fuel Share (G)",       //  6
                "Fuel Share (E85)",     //  7
                "Fuel Share (D)",       //  8
                "Fuel Share (E)",       //  9
                "Fuel Share (H)",       // 10
                "Fuel Share (CNG)"      // 11
            };
            string[] namesTech = TI.TechNamesVehicle;

            // scan each column to find the appropriate column names
            int[] indexesMain = this.DetectColumnIndexes(cols, namesMain);
            int[] indexesFuel = this.DetectColumnIndexes(cols, namesFuel);
            int[] indexesTech = this.DetectColumnIndexes(cols, namesTech);

            // check if all required columns were found
            if (!this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesMain, namesMain)
                || !this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesFuel, namesFuel)
                || !this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesTech, namesTech))
            {
                return null;
            }

            // ----- load the vehicle data -----
            List<BaselineVehicle> bVehs = new List<BaselineVehicle>();
            for (int i = 2; i < rows; i++)
            {
                var baselineInfo = this.LoadVehicle(i, indexesMain, indexesFuel, indexesTech, namesTech,
                                                    bMfrs, bPlts, bEngs, bTrns, mfrNames, out int mfrIndex);
                if (baselineInfo != null && mfrIndex != -1)
                {
                    bVehs.Add(baselineInfo);
                }
            }

            // we need to cross-reference and validate "ZEV Reference Vehicle"
            // only after all vehs have been loaded
            var bVehDict = new Dictionary<int, BaselineVehicle>();
            bVehs.ForEach(item => bVehDict[item.Code] = item);
            var zevVehs = bVehs.Where(item => item.IsZevCandidate && item.ZevReferenceVehicleCode != 0);
            foreach (var veh in zevVehs) {
                string hdr = $"Vehicle (Code:{veh.Code}):  ";
                if (!bVehDict.TryGetValue(veh.ZevReferenceVehicleCode, out var refVeh)) {
                    this.LogError($"{hdr}ZEV Reference Vehicle does not represent a valid vehicle code.");
                }
                if (refVeh != null && refVeh.ZEVCandidate.HasValue) {
                    this.LogError($"{hdr}ZEV Reference Vehicle may not itself be a ZEV Candidate.");
                }
            }

            return bVehs;
        }
        BaselineVehicle LoadVehicle(
            int row, int[] indexesMain, int[] indexesFuel, int[] indexesTech, string[] namesTech,
            List<BaselineManufacturer> bMfrs,
            List<Dictionary<string, object>> bPlts,
            List<Dictionary<string, object>> bEngs,
            List<Dictionary<string, object>> bTrns,
            List<string> mfrNames, out int mfrIndex)
        {
            mfrIndex = -1;

            //----- load data -----
            string vehCode      = this.GetString(row, indexesMain[ 0]);
            string mfrName      = this.GetString(row, indexesMain[ 1]);
            string brand        = this.GetTrimmedString(row, indexesMain[ 2]);
            string model        = this.GetTrimmedString(row, indexesMain[ 3]);
            string nameplate    = this.GetTrimmedString(row, indexesMain[ 4]);
            object pltCode      = this.GetData  (row, indexesMain[ 5]);
            object engCode      = this.GetData  (row, indexesMain[ 6]);
            object trnCode      = this.GetData  (row, indexesMain[ 7]);
            object sales        = this.GetData  (row, indexesMain[ 8]);
            object msrp         = this.GetData  (row, indexesMain[ 9]);
            string regClass     = this.GetString(row, indexesMain[10]);
            string regClass2    = this.GetString(row, indexesMain[11]);
            string techClass    = this.GetString(row, indexesMain[12]);
            string engTechClass = this.GetString(row, indexesMain[13]);
            string safetyClass  = this.GetString(row, indexesMain[14]);
            string zevCandidate = this.GetString(row, indexesMain[15]);
            string origin       = this.GetString(row, indexesMain[16]);
            string style        = this.GetString(row, indexesMain[17]);
            string drive        = this.GetString(row, indexesMain[18]);
            object footprint    = this.GetData  (row, indexesMain[19]);
            object cwMR0        = this.GetData  (row, indexesMain[20]);
            object curbWeight   = this.GetData  (row, indexesMain[21]);
            object gvwr         = this.GetData  (row, indexesMain[22]);
            object gcwr         = this.GetData  (row, indexesMain[23]);
            object maxGvwrCw    = this.GetData  (row, indexesMain[24]);
            object maxGcwrGvwr  = this.GetData  (row, indexesMain[25]);
            object fuelCapacity = this.GetData  (row, indexesMain[26]);
            object vehPower     = this.GetData  (row, indexesMain[27]);
            string refreshYrs   = this.GetString(row, indexesMain[28]);
            string redesignYrs  = this.GetString(row, indexesMain[29]);
            object dealerHrs    = this.GetData  (row, indexesMain[30]);
            object assemblyHrs  = this.GetData  (row, indexesMain[31]);
            object pctUSContent = this.GetData  (row, indexesMain[32]);
            object zevYear      = this.GetData  (row, indexesMain[33]);
            object zevRefVeh    = this.GetData  (row, indexesMain[34]);

            FuelValue fuelEconomy = FuelValue.Zero;
            FuelValue fuelShare = FuelValue.Zero;
            for (int i = 0; i < FuelTypes.List.Length; i++)
            {
                FuelType fuel = FuelTypes.List[i];
                fuelEconomy[fuel] = this.GetDouble(row, indexesFuel[i]);
                fuelShare[fuel] = this.GetDouble(row, indexesFuel[i + FuelTypes.List.Length]);
            }

            this.LoadTechApplicability(
                row, namesTech, indexesTech, out TechKey usedTechs, out TechKey availableTechs);

            // if vehicle code, mfr, brand, model, and nameplate columns are blank, assume the row is empty
            if (string.IsNullOrEmpty(vehCode) && string.IsNullOrEmpty(mfrName) && string.IsNullOrEmpty(brand)
                && string.IsNullOrEmpty(model) && string.IsNullOrEmpty(nameplate))
            {
                return null;
            }

            // skip the record under following conditions:
            //  - if the vehicle code is X-ed out (disabled by user)
            if (vehCode == "X")
            {
                return null;
            }

            //----- parse data -----
            int parsedVehCode = vehCode.GetInt32();
            int parsedPltCode = pltCode.GetInt32();
            int parsedEngCode = engCode.GetInt32();
            int parsedTrnCode = trnCode.GetInt32();
            double parsedSales = sales.GetDouble();
            
            RegulatoryClass ParseRegulatoryClass(string rc)
            {
                return (rc == "DC") ? RegulatoryClass.DomesticCar
                     : (rc == "IC") ? RegulatoryClass.ImportedCar
                     : (rc == "LT") ? RegulatoryClass.LightTruck
                     : (rc == "2B3" || rc == "LT2B3" || rc == "HDPUV") ? RegulatoryClass.HDPUV
                     : RegulatoryClass.None;
            }
            RegulatoryClass parsedRegClass  = ParseRegulatoryClass(regClass);
            RegulatoryClass parsedRegClass2 = ParseRegulatoryClass(regClass2);
            // NOTE: The unparsed string will be validated, but we want to allow for blank columns by copying the
            //       RC1 value to RC2.
            parsedRegClass2 = parsedRegClass2 == RegulatoryClass.None ? parsedRegClass : parsedRegClass2;
            
            TechnologyClass parsedTechClass = TechnologyClass.None;
            for (int i = 0; i < TechnologyClasses.List.Length; i++)
            {
                if (StringHelper.Equals(techClass, TechnologyClasses.Names[i], fuzzy: true))
                {
                    parsedTechClass = TechnologyClasses.List[i];
                    break;
                }
            }
            EngineTechnologyClass parsedEngTechClass = EngineTechnologyClass.None;
            for (int i = 0; i < TechnologyClasses.EngineList.Length; i++)
            {
                if (StringHelper.Equals(engTechClass, TechnologyClasses.EngineNames[i], fuzzy: true))
                {
                    parsedEngTechClass = TechnologyClasses.EngineList[i];
                    break;
                }
            }
            SafetyClass parsedSafetyClass = (safetyClass == "PC") ? SafetyClass.PC
                                          : (safetyClass == "LT") ? SafetyClass.LT
                                          : (safetyClass == "CM") ? SafetyClass.CM
                                          : SafetyClass.None;
            int? parsedZevCandidate = (zevCandidate.Length == 0) ? null : (int?)TI.GetIndex(zevCandidate);
            int parsedZevApplicationYear = zevYear.GetInt32();
            int parsedZevReferenceVehicleCode = zevRefVeh.GetInt32();

            Origin parsedOrigin = (origin == "I") ? Origin.Imported : Origin.Domestic;
            VehicleStyle parsedStyle = VehicleStyle.None;
            for (int i = 0; i < VehicleStyles.List.Length; i++)
            {
                if (StringHelper.Equals(style, VehicleStyles.Names[i], fuzzy: true)) {
                    parsedStyle = VehicleStyles.List[i];
                    break;
                }
                else if (StringHelper.Equals(style, "SUV", fuzzy: true)) {
                    // by default we expect "Sport Utility" identifier, but allow "SUV" shorthand also
                    parsedStyle = VehicleStyle.SportUtility;
                    break;
                }
            }
            DriveType parsedDrive = (drive == "F" || drive == "FWD") ? DriveType.FWD
                                  : (drive == "R" || drive == "RWD") ? DriveType.RWD
                                  : (drive == "2" || drive == "2WD") ? DriveType.FWD // assign FWD to 2WD for now
                                  : (drive == "4" || drive == "4WD") ? DriveType.x4WD
                                  : (drive == "A" || drive == "AWD") ? DriveType.AWD
                                  : DriveType.None;

            double parsedFootprint = footprint.GetDouble();
            double parsedCwMR0 = cwMR0.GetDouble();
            double parsedCurbWeight = curbWeight.GetDouble();
            double parsedGWVR = gvwr.GetDouble();
            double parsedGCVR = gcwr.GetDouble();
            double parsedFuelCapacity = fuelCapacity.GetDouble();

            var parsedRefreshYears = ModelYearKey.BuildKey(this.ParseRefreshRedesignYears(refreshYrs));
            var parsedRedesignYears = ModelYearKey.BuildKey(this.ParseRefreshRedesignYears(redesignYrs));

            // parse derived data -- variables that are not explicitly defined in input data
            VehicleClass parsedVehClass = VehicleClass.None;
            if (parsedStyle.IsCar())
            {
                parsedVehClass = VehicleClass.LDV;
            }
            else
            {
                parsedVehClass = (parsedGWVR >= 10001) ? VehicleClass.LDT3  :
                                 (parsedGWVR >=  8501) ? VehicleClass.LDT2b :
                                 (parsedGWVR >=  6001) ? VehicleClass.LDT2a :
                                                         VehicleClass.LDT1;
            }

            HEVType parsedHevType = HEVType.Conventional;
            for (int i = TI.TechnologyCount - 1; i >= 0; i--)
            {
                if (!usedTechs[i])
                    continue;

                if      (TI.IsConversionToMHEV(i)) { parsedHevType = HEVType.MildHybrid; break; }
                else if (TI.IsConversionToSHEV(i)) { parsedHevType = HEVType.StrongHybrid; break; }
                else if (TI.IsConversionToPHEV(i)) { parsedHevType = HEVType.PlugInHybrid; break; }
                else if (TI.IsConversionToEV  (i)) { parsedHevType = HEVType.BatteryElectric; break; }
                else if (TI.IsConversionToFCV (i)) { parsedHevType = HEVType.FuelCell; break; }
            }

            // compute aggregate list of used-techs
            TechKey GetComponentUsedTechs(List<Dictionary<string, object>> componentBaseInfos, int componentCode)
            {
                if (componentCode <= 0 || componentBaseInfos == null) {
                    return TechKey.Empty;
                }
                var componentBaseInfo = componentBaseInfos.Find(item => (int)item["Code"] == componentCode);
                return (componentBaseInfo == null) ? TechKey.Empty : (TechKey)componentBaseInfo["UsedTechnologies"];
            }
            TechKey GetComponentAvailTechs(List<Dictionary<string, object>> componentBaseInfos, int componentCode)
            {
                if (componentCode <= 0 || componentBaseInfos == null) {
                    return TechKey.Empty;
                }
                var componentBaseInfo = componentBaseInfos.Find(item => (int)item["Code"] == componentCode);
                return (componentBaseInfo == null) ? TechKey.Empty : (TechKey)componentBaseInfo["AvailableTechnologies"];
            }

            TechKey usedTechsFull = usedTechs
                | GetComponentUsedTechs(bPlts, parsedPltCode)
                | GetComponentUsedTechs(bEngs, parsedEngCode)
                | GetComponentUsedTechs(bTrns, parsedTrnCode);
            TechKey availableTechsFull = availableTechs
                | GetComponentAvailTechs(bPlts, parsedPltCode)
                | GetComponentAvailTechs(bEngs, parsedEngCode)
                | GetComponentAvailTechs(bTrns, parsedTrnCode);

            // for convenience, we also need to inherit the CARB aggrement
            // setting defined on a mfr for each vehicle model
            bool parsedCarbAgreement = false;
            var bMfr = bMfrs.Find(item => item.Name == mfrName);
            if (bMfr != null) {
                parsedCarbAgreement = bMfr.CARBAgreement;
            }

            //----- validate -----
            string hdr = "Vehicle (R:" + (row + 1).ToString("0000") + "):  ";

            mfrIndex = mfrNames.IndexOf(mfrName);
            if (mfrIndex == -1) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Manufacturer"); }

            if (fuelEconomy.IsEmpty) {
                this.LogError($"{hdr}Fuel Economy is not valid (at least one Fuel Economy value must be specified).");
            }
            else if (!fuelEconomy.IsValid) {
                this.LogError($"{hdr}Fuel Economy is not valid (the Fuel Economy value cannot be negative).");
            }

            if (fuelShare.IsEmpty) {
                this.LogError($"{hdr}Fuel Share is not valid (at least one Fuel Share value must be specified).");
            }
            else if (!fuelShare.IsValid) {
                this.LogError($"{hdr}Fuel Share is not valid (the Fuel Share value cannot be negative).");
            }
            else if (fuelShare.Total != 1) {
                this.LogError($"{hdr}Fuel Share is not valid (Fuel Shares for all fuel types must add up to 100%).");
            }

            foreach (FuelType fuelType in FuelTypes.List)
            {
                if ((fuelEconomy[fuelType] == 0 && fuelShare[fuelType] != 0) ||
                    (fuelEconomy[fuelType] != 0 && fuelShare[fuelType] == 0))
                {
                    this.LogError($"{hdr}{fuelType} Fuel Economy and Fuel Share do not correspond.");
                }
            }

            // Notes on engines/transmissions:
            //  * Some vehicles cannot have an engine or transmission present (e.g., BEVs).
            //  * Some other vehicles have unique engine/transmission and should not be specified
            //    in inputs for modeling purposes.
            //  * For simplicity, however, we allow users to specify engines and transmissions on
            //    most vehicles (and then simply ignore them) unless they are incompatible.
            //  * The following provides the engine/transmission logic we accept:
            //      - all conventional vehicles and MHEVs require an engine and a transmission;
            //      - SHEVs do not require an engine or a transmission, but one may be defined;
            //      - PHEVs, BEVs, and FCVs may not have a transmission defined; and
            //      - BEVs and FCVs may not have an engine defined.
            bool requireTrn = (parsedHevType == HEVType.Conventional
                            || parsedHevType == HEVType.MildHybrid);
            bool requireEng = (parsedHevType == HEVType.Conventional
                            || parsedHevType == HEVType.MildHybrid);
            if (!requireTrn) { parsedTrnCode = 0; }
            if (!requireEng) { parsedEngCode = 0; }

            bool skipTrn = (parsedHevType == HEVType.PlugInHybrid
                         || parsedHevType == HEVType.BatteryElectric
                         || parsedHevType == HEVType.FuelCell);
            bool skipEng = (parsedHevType == HEVType.BatteryElectric
                         || parsedHevType == HEVType.FuelCell);

            if (!vehCode.IsInteger() || parsedVehCode < 1) {
                this.LogError(hdr + "Vehicle Code is not specified or not valid.");
            }
            if (brand.Length == 0) { this.LogError(hdr + "Brand is not specified."); }
            if (model.Length == 0) { this.LogError(hdr + "Model is not specified."); }
            if (nameplate.Length == 0) { this.LogError(hdr + "Nameplate is not specified."); }

            if (!pltCode.IsInteger() || parsedPltCode < 1) {
                this.LogError(hdr + "Platform Code is not specified or not valid.");
            }

            if (parsedEngCode < 1 && requireEng) {
                this.LogError($"{hdr}Engine Code is not specified or not valid.");
            }
            if (parsedEngCode > 0 && skipEng) {
                this.LogError($"{hdr}Engine Code cannot be specified for BEVs or FCVs.");
            }
            if (parsedTrnCode < 1 && requireTrn) {
                this.LogError($"{hdr}Transmission Code is not specified or not valid.");
            }
            if (parsedTrnCode > 0 && skipTrn) {
                this.LogError($"{hdr}Transmission Code cannot be specified for PHEVs, BEVs, or FCVs.");
            }

            if (sales.ToString().Trim().Length > 0 && !sales.IsNumeric()) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Sales"); }
            if (!msrp.IsNumeric()) { this.LogErrorNotSpecifiedOrNotValid(hdr, "MSRP"); }

            if (parsedRegClass == RegulatoryClass.None) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Regulatory Class"); }
            if (!ValidRcStrings.Contains(regClass2)) { this.LogErrorNotValid(hdr, "Regulatory Class 2"); }
            if (parsedTechClass == TechnologyClass.None) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Technology Class"); }
            if (parsedEngTechClass == EngineTechnologyClass.None) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Engine Technology Class"); }
            if (parsedSafetyClass == SafetyClass.None) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Safety Class"); }

            if (parsedZevCandidate.HasValue) {
                if (parsedZevCandidate == -1) {
                    this.LogError($"{hdr}ZEV Candidate does not represent a valid technology.");
                }
                else if (!TI.IsConversionToZEV(parsedZevCandidate.Value)) {
                    this.LogError($"{hdr}ZEV Candidate must be one of the predefined PHEV, BEV, or FCV technologies.");
                }
                else {
                    // ensure "ZEV candidate" technology is more advanced than vehicle's current state
                    HEVType zevHevType = TI.IsConversionToPHEV(parsedZevCandidate.Value) ? HEVType.PlugInHybrid
                                       : TI.IsConversionToEV  (parsedZevCandidate.Value) ? HEVType.BatteryElectric
                                       : TI.IsConversionToFCV (parsedZevCandidate.Value) ? HEVType.FuelCell
                                       : HEVType.Conventional;

                    if (zevHevType == parsedHevType) {
                        this.LogError($"{hdr}ZEV Candidate technology's HEV type may not be the same as vehicle's HEV type.");
                    }
                    else if (zevHevType < parsedHevType) {
                        this.LogError($"{hdr}ZEV Candidate technology's HEV type must be more advanced than vehicle's HEV type.");
                    }
                }

                if (parsedZevApplicationYear < ModelYearKey.MinYear) {
                    this.LogErrorNotSpecifiedOrNotValid(hdr, "ZEV Application Year");
                }

                // ZEV Reference Vehicle is optional, but if specified must be an integer >=1
                if (zevRefVeh.ToString().Trim().Length > 0
                        && (!zevRefVeh.IsInteger() || parsedZevReferenceVehicleCode < 1)) {
                    this.LogErrorNotSpecifiedOrNotValid(hdr, "ZEV Reference Vehicle");
                }
            }
            else {
                // if ZEV candidate is not specified, we do not allow ZEV app year or ref vehicle
                if (zevYear.ToString().Trim().Length > 0) {
                    this.LogError($"{hdr}ZEV Application Year may not be specified if ZEV Candidate is not defined.");
                }
                if (zevRefVeh.ToString().Trim().Length > 0) {
                    this.LogError($"{hdr}ZEV Reference Vehicle may not be specified if ZEV Candidate is not defined.");
                }
            }

            if (origin.Length == 0) { origin = "D"; }
            if (origin != "I" && origin != "D") { this.LogErrorNotSpecifiedOrNotValid(hdr, "Origin"); }
            if (parsedStyle == VehicleStyle.None) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Style"); }
            if (parsedDrive == DriveType.None) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Drive"); }

            // some vehicle characteristics are optional (some are optional depending on reg-class)
            // however, if they are specified, they must be valid
            string footprintStr = footprint.ToString().Trim();
            string gvwrStr = gvwr.ToString().Trim();
            string gcwrStr = gcwr.ToString().Trim();
            string fuelCapacityStr = fuelCapacity.ToString().Trim();

            if (footprintStr.Length > 0 && !footprint.IsNumeric()) { this.LogError($"{hdr}Footprint is not valid."); }
            if (!parsedRegClass.IsHDPUV() && footprintStr.Length == 0) { this.LogError($"{hdr}Footprint is not specified."); }
            if (!curbWeight.IsNumeric()) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Curb Weight"); }
            if (!cwMR0.IsNumeric()) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Curb Weight (MR0)"); }
            if (parsedCwMR0 < parsedCurbWeight) { this.LogError($"{hdr}Curb Weight (MR0) must be >= Curb Weight."); }
            if (gvwrStr.Length > 0 && !gvwr.IsNumeric()) { this.LogError($"{hdr}GVWR is not valid."); }
            if ((parsedRegClass.IsHDPUV() || parsedRegClass.IsLT()) && gvwrStr.Length == 0) {
                this.LogError($"{hdr}GVWR is not specified.");
            }
            if (gcwrStr.Length > 0 && !gcwr.IsNumeric()) { this.LogError($"{hdr}GCWR is not valid."); }
            if (parsedRegClass.IsHDPUV() && gvwrStr.Length == 0) { this.LogError($"{hdr}GCWR is not specified."); }
            if (maxGvwrCw.ToString().Trim().Length > 0 && !maxGvwrCw.IsNumeric()) {
                this.LogError($"{hdr}Max GVWR/CW is not valid.");
            }
            if (maxGcwrGvwr.ToString().Trim().Length > 0 && !maxGcwrGvwr.IsNumeric()) {
                this.LogError($"{hdr}Max GCWR/GVWR is not valid.");
            }
            if (fuelCapacityStr.Length > 0 && !fuelCapacity.IsNumeric()) {
                this.LogError($"{hdr}Fuel Capacity is not valid.");
            }
            if (!fuelShare.FuelType.IsGEGFuel() && fuelCapacityStr.Length == 0) {
                this.LogError($"{hdr}Fuel Capacity is not specified.");
            }
            if (!vehPower.IsNumeric()) { this.LogErrorNotSpecifiedOrNotValid(hdr, "Vehicle Power"); }

            this.ValidateRefreshRedesignYears(hdr, refreshYrs, "Refresh Years");
            this.ValidateRefreshRedesignYears(hdr, redesignYrs, "Redesign Years");

            if (dealerHrs.ToString().Trim().Length > 0 && !dealerHrs.IsNumeric()) {
                this.LogError($"{hdr}Dealership Employment Hours is not valid.");
            }
            if (assemblyHrs.ToString().Trim().Length > 0 && !assemblyHrs.IsNumeric()) {
                this.LogError($"{hdr}US Assembly Employment Hours is not valid.");
            }
            if (pctUSContent.ToString().Trim().Length > 0 && !pctUSContent.IsNumeric()) {
                this.LogError($"{hdr}Percent US Content is not valid.");
            }

            //----- init and return data -----
            var values = new Dictionary<string, object>
            {
                ["Code"] = parsedVehCode,
                ["ManufacturerName"] = mfrName,
                ["Brand"] = brand,
                ["Model"] = model,
                ["Nameplate"] = nameplate,
                ["PlatformCode"] = parsedPltCode,
                ["EngineCode"] = parsedEngCode,
                ["TransmissionCode"] = parsedTrnCode,
                ["FuelEconomy"] = fuelEconomy,
                ["FuelShare"] = fuelShare,
                ["Sales"] = parsedSales,
                ["Msrp"] = msrp.GetDouble(),
                ["RegClass"] = parsedRegClass,
                ["RegClass2"] = parsedRegClass2,
                ["SafetyClass"] = parsedSafetyClass,
                ["VehicleClass"] = parsedVehClass,
                ["TechnologyClass"] = parsedTechClass,
                ["EngineTechnologyClass"] = parsedEngTechClass,
                ["ZEVCandidate"] = parsedZevCandidate,
                ["ZevApplicationYear"] = parsedZevApplicationYear,
                ["ZevReferenceVehicleCode"] = parsedZevReferenceVehicleCode,
                ["CARBAgreement"] = parsedCarbAgreement,
                ["Origin"] = parsedOrigin,
                ["Style"] = parsedStyle,
                ["Structure"] = VehicleFrame.None,
                ["Drive"] = parsedDrive,
                ["Footprint"] = parsedFootprint,
                ["CurbWeightMR0"] = parsedCwMR0,
                ["CurbWeight"] = parsedCurbWeight,
                ["GVWR"] = parsedGWVR,
                ["GCWR"] = parsedGCVR,
                ["MaxGVWRToCW"] = maxGvwrCw.GetDouble(),
                ["MaxGCWRToGVWR"] = maxGcwrGvwr.GetDouble(),
                ["FuelCapacity"] = parsedFuelCapacity,
                ["HevType"] = parsedHevType,
                ["VehiclePower"] = vehPower.GetDouble(),
                ["VehicleTorque"] = 0,  // we do not currently have
                ["VehicleRange"] = 0,   // veh torque or range in inputs
                ["RefreshYears"] = parsedRefreshYears,
                ["RedesignYears"] = parsedRedesignYears,
                ["DealershipHours"] = dealerHrs.GetDouble(),
                ["USAssemblyHours"] = assemblyHrs.GetDouble(),
                ["PercentUSContent"] = pctUSContent.GetDouble(),
                ["UsedTechnologies"] = usedTechs,
                ["AvailableTechnologies"] = availableTechs,
                ["UsedTechnologiesFull"] = usedTechsFull,
                ["AvailableTechnologiesFull"] = availableTechsFull
            };
            var baselineInfo = new BaselineVehicle(values);
            return baselineInfo;
        }

        // ----- Methods for parsing platforms from the vehicles worksheet -----
        List<Dictionary<string, object>> LoadPlatforms(int index, List<string> mfrNames)
        {
            this.ActivateWorksheet(index);

            // ----- detect column indexes -----
            int rows = this.Rows;
            int cols = this.Columns;
            string[] namesMain = new string[] {
                "Platform Code",    // 0
                "Manufacturer",     // 1
                "Name",             // 2
                "Family Name",      // 3
                "Refresh Years",    // 4
                "Redesign Years"    // 5
            };
            string[] namesTech = TI.TechNamesPlatform;

            // scan each column to find the appropriate column names
            int[] indexesMain = this.DetectColumnIndexes(cols, namesMain);
            int[] indexesTech = this.DetectColumnIndexes(cols, namesTech);

            // check if all required columns were found
            if (!this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesMain, namesMain)
                || !this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesTech, namesTech))
            {
                return null;
            }

            // ----- load the platform data -----
            var bList = new List<Dictionary<string, object>>();
            for (int i = 2; i < rows; i++) {
                var bInfo = this.LoadPlatform(i, indexesMain, indexesTech, namesTech, mfrNames, out int mfrIndex);
                if (bInfo != null && mfrIndex != -1) {
                    bList.Add(bInfo);
                }
            }
            return bList;
        }
        Dictionary<string, object> LoadPlatform(
            int row, int[] indexesMain, int[] indexesTech, string[] namesTech, List<string> mfrNames,
            out int mfrIndex)
        {
            mfrIndex = -1;

            //----- load data -----
            // (platforms are defined on the vehicles worksheet)
            string pltCode     = this.GetString(row, indexesMain[0]);
            string mfrName     = this.GetString(row, indexesMain[1]);
            string name        = this.GetString(row, indexesMain[2]);
            string family      = this.GetString(row, indexesMain[3]);
            string refreshYrs  = this.GetString(row, indexesMain[4]);
            string redesignYrs = this.GetString(row, indexesMain[5]);

            this.LoadTechApplicability(
                row, namesTech, indexesTech, out TechKey usedTechs, out TechKey availableTechs);

            // if platform code, mfr, and name columns are blank, assume the row is empty
            if (string.IsNullOrEmpty(pltCode) && string.IsNullOrEmpty(mfrName) && string.IsNullOrEmpty(name))
            {
                return null;
            }

            // skip the record under following conditions:
            //  - if the platform code is X-ed out (disabled by user)
            if (pltCode == "X")
            {
                return null;
            }

            //----- parse data -----
            int parsedPltCode = pltCode.GetInt32();

            //----- validate -----
            string hdr = "Platform (R:" + (row + 1).ToString("0000") + "):  ";
            mfrIndex = mfrNames.IndexOf(mfrName);
            if (mfrIndex == -1) { this.LogError(hdr + "Manufacturer is not specified or not valid."); }
            if (!pltCode.IsInteger() || parsedPltCode < 1) {
                this.LogError(hdr + "Platform Code is not specified or not valid.");
            }
            if (name.Length == 0) { this.LogError(hdr + "Name is not specified."); }

            this.ValidateComponentRefreshRedesignYears(hdr, refreshYrs, "Refresh Years");
            this.ValidateComponentRefreshRedesignYears(hdr, redesignYrs, "Redesign Years");

            //----- init and return data -----
            var values = new Dictionary<string, object>
            {
                ["Code"] = parsedPltCode,
                ["ManufacturerName"] = mfrName,
                ["Name"] = name,
                ["ComponentName"] = name, // platform's "component" name must be same as name
                ["FamilyName"] = family,
                ["RefreshYears"] = refreshYrs,
                ["RedesignYears"] = redesignYrs,
                ["UsedTechnologies"] = usedTechs,
                ["AvailableTechnologies"] = availableTechs
            };
            return values;
            //var baselineInfo = new BaselinePlatform(values);
            //return baselineInfo;
        }

        // ----- Methods for parsing the engines worksheet -----
        List<Dictionary<string, object>> LoadEngines(int index, List<string> mfrNames)
        {
            this.ActivateWorksheet(index);

            // ----- detect column indexes -----
            int rows = this.Rows;
            int cols = this.Columns;
            string[] namesMain = {
                "Engine Code",          //  0
                "Manufacturer",         //  1
                "Family Name",          //  2
                "Fuel",                 //  3
                "Valvetrain Design",    //  4
                "Displacement",         //  5
                "Configuration",        //  6
                "Cylinders",            //  7
                "Aspiration",           //  8
                "Refresh Years",        //  9
                "Redesign Years"        // 10
            };
            string[] namesTech = TI.TechNamesEngine;

            // scan each column to find the appropriate column names
            int[] indexesMain = this.DetectColumnIndexes(cols, namesMain);
            int[] indexesTech = this.DetectColumnIndexes(cols, namesTech);

            // check if all required columns were found
            if (!this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesMain, namesMain)
                || !this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesTech, namesTech))
            {
                return null;
            }

            // ----- load the engine data -----
            var bList = new List<Dictionary<string, object>>();
            for (int i = 2; i < rows; i++) {
                var bInfo = this.LoadEngine(i, indexesMain, indexesTech, namesTech, mfrNames, out int mfrIndex);
                if (bInfo != null && mfrIndex != -1) {
                    bList.Add(bInfo);
                }
            }
            return bList;
        }
        Dictionary<string, object> LoadEngine(
            int row, int[] indexesMain, int[] indexesTech, string[] namesTech, List<string> mfrNames,
            out int mfrIndex)
        {
            mfrIndex = -1;

            //----- load data -----
            string engCode       = this.GetString(row, indexesMain[0]);
            string mfrName       = this.GetString(row, indexesMain[1]);
            string family        = this.GetString(row, indexesMain[2]);
            string engFuel       = this.GetString(row, indexesMain[3]);
            string valvetrain    = this.GetString(row, indexesMain[4]);
            object displacement  = this.GetData  (row, indexesMain[5]);
            string configuration = this.GetString(row, indexesMain[6]);
            object cylinders     = this.GetData  (row, indexesMain[7]);
            string aspiration    = this.GetString(row, indexesMain[8]);
            string refreshYrs    = this.GetString(row, indexesMain[9]);
            string redesignYrs   = this.GetString(row, indexesMain[10]);

            this.LoadTechApplicability(
                row, namesTech, indexesTech, out TechKey usedTechs, out TechKey availableTechs);

            // if engine code, mfr, and fuel columns are blank, assume the row is empty
            if (string.IsNullOrEmpty(engCode) && string.IsNullOrEmpty(mfrName) && string.IsNullOrEmpty(engFuel))
            {
                return null;
            }

            // skip the record under following conditions:
            //  - if the engine code is X-ed out (disabled by user)
            //  - if engine  fuel is specified as "DD" (direct-drive), since that coding is a
            //    placeholder for BEVs and FCVs
            //  - if engine fuel is explicitly defined as "E" (electricity) or "H" (hydrogen),
            //    since those are also placeholders for BEVs and FCVs
            if (engCode == "X" || engFuel == "DD" || engFuel == "E" || engFuel == "H")
            {
                return null;
            }

            //----- parse data -----
            int parsedEngCode = engCode.GetInt32();
            FuelType parsedFuel = (engFuel == "G" || engFuel == "G+E85") ? FuelType.Gasoline
                                : (engFuel == "D" || engFuel == "D+B20") ? FuelType.Diesel
                                : (engFuel == "CNG") ? FuelType.CNG
                                : FuelType.None;
            EngineValvetrain parsedValvetrain = (valvetrain == "OHV") ? EngineValvetrain.OHV
                                              : (valvetrain == "SOHC") ? EngineValvetrain.SOHC
                                              : (valvetrain == "DOHC") ? EngineValvetrain.DOHC
                                              : EngineValvetrain.None;
            EngineConfiguration parsedConfiguration = (configuration == "I") ? EngineConfiguration.I
                                                    : (configuration == "V") ? EngineConfiguration.V
                                                    : (configuration == "H") ? EngineConfiguration.H
                                                    : (configuration == "W") ? EngineConfiguration.W
                                                    : (configuration == "R") ? EngineConfiguration.R
                                                    : EngineConfiguration.None;
            EngineAspiration parsedAspiration =
                  (aspiration == "NA") ? EngineAspiration.NaturallyAspirated
                : StringHelper.EqualsAny(aspiration, new string[] { "S", "ST" }) ? EngineAspiration.Supercharger
                : StringHelper.EqualsAny(aspiration, new string[] { "T", "T2", "T4", "TS" }) ? EngineAspiration.Turbocharger
                : EngineAspiration.None;

            //----- validate -----
            string hdr = "Engine (R:" + (row + 1).ToString("0000") + "):  ";

            mfrIndex = mfrNames.IndexOf(mfrName);
            if (mfrIndex == -1) { this.LogError(hdr + "Manufacturer is not specified or not valid."); }
            if (!engCode.IsInteger() || parsedEngCode < 1) {
                this.LogError(hdr + "Engine Code is not specified or not valid.");
            }
            if (!displacement.IsNumeric()) { this.LogError(hdr + "Displacement is not specified or not valid."); }
            if (!cylinders.IsInteger()) { this.LogError(hdr + "Number of Cylinders is too small."); }

            if (parsedFuel == FuelType.None) {
                this.LogError(hdr + "Engine Fuel is not specified or not valid.");
            }
            if (parsedConfiguration != EngineConfiguration.R && parsedValvetrain == EngineValvetrain.None) {
                this.LogError(hdr + "Engine Valvetrain Design is not specified or not valid.");
            }
            if (parsedConfiguration == EngineConfiguration.None) {
                this.LogError(hdr + "Engine Configuration is not specified or not valid.");
            }
            if (parsedAspiration == EngineAspiration.None) {
                this.LogError(hdr + "Engine Aspiration is not specified or not valid.");
            }
            this.ValidateComponentRefreshRedesignYears(hdr, refreshYrs, "Refresh Years");
            this.ValidateComponentRefreshRedesignYears(hdr, redesignYrs, "Redesign Years");

            //----- init and return data -----
            var values = new Dictionary<string, object>
            {
                ["Code"] = parsedEngCode,
                ["ManufacturerName"] = mfrName,
                ["FamilyName"] = family,
                ["Fuel"] = parsedFuel,
                ["ValvetrainDesign"] = parsedValvetrain,
                ["Displacement"] = displacement.GetDouble(),
                ["Configuration"] = parsedConfiguration,
                ["Cylinders"] = cylinders.GetInt32(),
                ["Aspiration"] = parsedAspiration,
                ["RefreshYears"] = refreshYrs,
                ["RedesignYears"] = redesignYrs,
                ["UsedTechnologies"] = usedTechs,
                ["AvailableTechnologies"] = availableTechs
            };
            return values;
            //var baselineInfo = new BaselineEngine(values);
            //return baselineInfo;
        }

        // ----- Methods for parsing the transmissions worksheet -----
        List<Dictionary<string, object>> LoadTransmissions(int index, List<string> mfrNames)
        {
            this.ActivateWorksheet(index);

            // ----- detect column indexes -----
            int rows = this.Rows;
            int cols = this.Columns;
            string[] namesMain = {
                "Transmission Code",        // 0
                "Manufacturer",             // 1
                "Family Name",              // 2
                "Type",                     // 3
                "Number of Forward Gears",  // 4
                "Refresh Years",            // 5
                "Redesign Years"            // 6
            };
            string[] namesTech = TI.TechNamesTransmission;

            // scan each column to find the appropriate column names
            int[] indexesMain = this.DetectColumnIndexes(cols, namesMain);
            int[] indexesTech = this.DetectColumnIndexes(cols, namesTech);

            // check if all required columns were found
            if (!this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesMain, namesMain)
                || !this.VerifyIndexes(this.SheetNames[index] + " worksheet", indexesTech, namesTech))
            {
                return null;
            }

            // ----- load the transmission data -----
            var bList = new List<Dictionary<string, object>>();
            for (int i = 2; i < rows; i++) {
                var bInfo = this.LoadTransmission(i, indexesMain, indexesTech, namesTech, mfrNames, out int mfrIndex);
                if (bInfo != null && mfrIndex != -1) {
                    bList.Add(bInfo);
                }
            }
            return bList;
        }
        Dictionary<string, object> LoadTransmission(
            int row, int[] indexesMain, int[] indexesTech, string[] namesTech, List<string> mfrNames,
            out int mfrIndex)
        {
            mfrIndex = -1;

            //----- load data -----
            string trnCode     = this.GetString(row, indexesMain[0]);
            string mfrName     = this.GetString(row, indexesMain[1]);
            string family      = this.GetString(row, indexesMain[2]);
            string trnType     = this.GetString(row, indexesMain[3]);
            object numGears    = this.GetData  (row, indexesMain[4]);
            string refreshYrs  = this.GetString(row, indexesMain[5]);
            string redesignYrs = this.GetString(row, indexesMain[6]);

            this.LoadTechApplicability(
                row, namesTech, indexesTech, out TechKey usedTechs, out TechKey availableTechs);

            // if transmission code, mfr, and type columns are blank, assume the row is empty
            if (string.IsNullOrEmpty(trnCode) && string.IsNullOrEmpty(mfrName) && string.IsNullOrEmpty(trnType))
            {
                return null;
            }

            // skip the record under following conditions:
            //  - if the transmission code is X-ed out (disabled by user)
            //  - if transmission  type is specified as "DD" (direct-drive), since that coding is
            //    a placeholder for BEVs and FCVs
            if (trnCode == "X" || trnType == "DD")
            {
                return null;
            }

            //----- parse data -----
            int parsedTrnCode = trnCode.GetInt32();
            TransmissionType parsedTrnType =
                  (trnType == "A" || trnType == "AT") ? TransmissionType.A
                : (trnType == "M" || trnType == "MT") ? TransmissionType.M
                : (trnType == "CVT") ? TransmissionType.CVT
                : (trnType == "AMT") ? TransmissionType.AMT
                : (trnType == "DCT") ? TransmissionType.DCT
                : TransmissionType.None;
            int parsedNumGears = numGears.GetInt32();
            if (parsedTrnType == TransmissionType.CVT && parsedNumGears == 0)
            {
                parsedNumGears = 1;
            }

            //----- validate -----
            string hdr = "Transmission (R:" + (row + 1).ToString("0000") + "):  ";

            mfrIndex = mfrNames.IndexOf(mfrName);
            if (mfrIndex == -1) { this.LogError(hdr + "Manufacturer is not specified or not valid."); }
            if (!trnCode.IsInteger() || parsedTrnCode < 1) {
                this.LogError(hdr + "Transmission Code is not specified or not valid.");
            }
            if (!numGears.IsInteger() || parsedNumGears < 1) {
                this.LogError(hdr + "Number of Forward Gears is not specified or not valid.");
            }

            if (parsedTrnType == TransmissionType.None) {
                this.LogError(hdr + "Transmission Type is not specified or not valid.");
            }
            else if (parsedTrnType != TransmissionType.CVT && parsedNumGears < 3) {
                this.LogError(hdr + "Number of Forward Gears is too small.");
            }

            this.ValidateComponentRefreshRedesignYears(hdr, refreshYrs, "Refresh Years");
            this.ValidateComponentRefreshRedesignYears(hdr, redesignYrs, "Redesign Years");

            //----- init and return data -----
            var values = new Dictionary<string, object>
            {
                ["Code"] = parsedTrnCode,
                ["ManufacturerName"] = mfrName,
                ["FamilyName"] = family,
                ["Type"] = parsedTrnType,
                ["NumForwardGears"] = parsedNumGears,
                ["RefreshYears"] = refreshYrs,
                ["RedesignYears"] = redesignYrs,
                ["UsedTechnologies"] = usedTechs,
                ["AvailableTechnologies"] = availableTechs
            };
            return values;
            //var baselineInfo = new BaselineTransmission(values);
            //return baselineInfo;
        }

        // ----- Methods for parsing the credits and adjustments worksheet -----
        CreditsAndAdjustments[] LoadCreditsAndAdjustments(int index, List<string> mfrNames)
        {
            var crAdj = new CreditsAndAdjustments[mfrNames.Count];

            // activate the worksheet
            this.ActivateWorksheet(index);

            // init variables needed for parsing
            string sheetName = this.SheetNames[index];
            var mfrRows = new List<string> { "Name", "AC Efficiency", "AC Leakage", "Off-Cycle Credits", "FSP Credits",
                "FFV Credits" };
            int regClassRow = 0;
            int startMfrRow = 2;
            int mfrCol = 0;
            int startModelYearCol = mfrCol + 1;

            if (this.ValidateCreditsAndAdjustmentsRows(startMfrRow, mfrCol, mfrNames, mfrRows, sheetName) &&
                this.ValidateCreditsAndAdjustmentsColumns(regClassRow, startModelYearCol, sheetName, out int minMY, out int maxMY))
            {
                // Sheet is valid - proceed with parsing
                int numRows = this.Rows;
                int numCols = this.Columns;

                int numCreditYears = maxMY - minMY + 1;
                int numRowsPerMfr = mfrRows.Count;

                for (int currRow = startMfrRow; currRow < numRows; currRow += numRowsPerMfr)
                {
                    CreditsAndAdjustmentsInfo[] passengerCar = null;
                    CreditsAndAdjustmentsInfo[] lightTruck = null;
                    CreditsAndAdjustmentsInfo[] hdpuv = null;
                    for (int currCol = startModelYearCol; currCol < numCols; currCol += numCreditYears)
                    {
                        string regClassName = this.GetString(regClassRow, currCol);
                        CreditsAndAdjustmentsInfo[] regInfo;
                        if (regClassName == "PASSENGER CAR")
                        {
                            passengerCar = new CreditsAndAdjustmentsInfo[numCreditYears];
                            regInfo = passengerCar;
                        }
                        else if (regClassName == "HDPUV")
                        {
                            hdpuv = new CreditsAndAdjustmentsInfo[numCreditYears];
                            regInfo = hdpuv;
                        }
                        else if (regClassName == "LIGHT TRUCK")
                        {
                            lightTruck = new CreditsAndAdjustmentsInfo[numCreditYears];
                            regInfo = lightTruck;
                        }
                        else {
                            regInfo = null;
                        }

                        int lastRegClassCol = currCol + numCreditYears;
                        for (int modelYearCol = currCol; modelYearCol < lastRegClassCol; ++modelYearCol)
                        {
                            bool validRecord = true;
                            // Name
                            double nameVal = this.GetDouble(currRow + 0, modelYearCol, -1);
                            if (nameVal != -1)
                            {
                                this.LogError($"{sheetName} (R:{currRow + 1:0000}): Manufacturer name row should not have any values.");
                                validRecord = false;
                            }
                            // AC Efficiency
                            double acEffVal = this.GetDouble(currRow + 1, modelYearCol);
                            if (acEffVal < 0)
                            {
                                validRecord = false;
                                this.LogError($"{sheetName} (R:{currRow + 2:0000}): AC Efficiency values cannot be negative.");
                            }
                            // AC Leakage
                            double acLeakVal = this.GetDouble(currRow + 2, modelYearCol);
                            if (acLeakVal < 0)
                            {
                                validRecord = false;
                                this.LogError($"{sheetName} (R:{currRow + 3:0000}): AC Leakage values cannot be negative.");
                            }
                            // Off-Cycle Credits
                            double occVal = this.GetDouble(currRow + 3, modelYearCol);
                            if (occVal < 0)
                            {
                                validRecord = false;
                                this.LogError($"{sheetName} (R:{currRow + 4:0000}): Off-Cycle Credits values cannot be negative.");
                            }
                            // FSP Credits
                            double fspVal = this.GetDouble(currRow + 4, modelYearCol);
                            if (occVal < 0) {
                                validRecord = false;
                                this.LogError($"{sheetName} (R:{currRow + 5:0000}): FSP Credits values cannot be negative.");
                            }
                            // FFV Credits
                            double ffvVal = this.GetDouble(currRow + 5, modelYearCol);
                            if (ffvVal < 0)
                            {
                                validRecord = false;
                                this.LogError($"{sheetName} (R:{currRow + 6:0000}): FFV Credits values cannot be negative.");
                            }

                            if (validRecord) {
                                regInfo[modelYearCol - currCol] =
                                    new CreditsAndAdjustmentsInfo(acEffVal, acLeakVal, occVal, fspVal, ffvVal);
                            }
                        }
                    }

                    string mfrName = this.GetString(currRow, mfrCol);
                    int mfrIndex = mfrNames.IndexOf(mfrName);
                    var crAdjInfo = new RCAltValue<CreditsAndAdjustmentsInfo[]>(passengerCar, lightTruck, hdpuv);
                    crAdj[mfrIndex] = new CreditsAndAdjustments(minMY, maxMY, crAdjInfo);
                }
            }

            return crAdj;
        }
        bool ValidateCreditsAndAdjustmentsRows(
            int startMfrRow, int mfrCol, List<string> mfrNames, List<string> mfrRows, string sheetName)
        {
            // - manufacturers are listed with no empty rows in between them
            // - each manufacturer takes number of rows specified in the mfrRows param
            // - manufacturer names from the "Manufacturers" sheet and here must match

            bool rowsValid = true;

            int numRows = this.Rows;
            for (int row = startMfrRow; row < numRows; ++row)
            {
                string mfrStr = this.GetString(row, mfrCol);
                if (string.IsNullOrWhiteSpace(mfrStr))
                {
                    rowsValid = false;
                    this.LogError($"{sheetName}: The specified worksheet does not contain any valid entries.");
                    break;
                }
            }

            if (rowsValid)
            {
              //  int numMfrRows = numRows - startMfrRow;
                int numRowsPerMfr = mfrRows.Count;

                var allMfrs = new List<string>();
                for (int row = startMfrRow; row < numRows; row += numRowsPerMfr)
                {
                    string hdr = $"{sheetName} (R:{row + 1:0000}):  ";
                    string mfrName = this.GetString(row, mfrCol);

                    if (allMfrs.Contains(mfrName))
                    {
                        rowsValid = false;
                        this.LogError($"{hdr}Duplicate manufacturer definition detected for: '{mfrName}'.");
                    }
                    else if (mfrNames.IndexOf(mfrName) == -1)
                    {
                        rowsValid = false;
                        this.LogError($"{hdr}The manufacturer '{mfrName}' is not specified or not valid.");
                    }
                    else
                    {
                        allMfrs.Add(mfrName);
                    }

                    for (int innerMfrRow = 1; innerMfrRow < numRowsPerMfr; ++innerMfrRow)
                    {
                        string rowValue = this.GetString(row + innerMfrRow, mfrCol);
                        if (string.Equals(rowValue, mfrRows[innerMfrRow], StringComparison.OrdinalIgnoreCase) == false)
                        {
                            rowsValid = false;
                            this.LogError($"{hdr}The manufacturer '{mfrName}' contains missing or improperly formatted data.");
                        }
                    }
                }

                // check for a match with manufacturers tab
                foreach (string mfrName in mfrNames)
                {
                    if (!allMfrs.Contains(mfrName))
                    {
                        rowsValid = false;
                        this.LogError($"The manufacturer '{mfrName}' was not found on the '{sheetName}' sheet.");
                    }
                }
            }

            return rowsValid;
        }
        bool ValidateCreditsAndAdjustmentsColumns(
            int regClassRow, int startModelYearCol, string sheetName, out int minMY, out int maxMY)
        {
            minMY = maxMY = 0;
            bool colsValid = true;

            int numCols = this.Columns;
            int modelYearRow = regClassRow + 1;

            var regClasses = new Dictionary<string, List<int>>();
            string currKey = string.Empty;
            for (int col = startModelYearCol; col < numCols; ++col)
            {
                string regClassName = this.GetString(regClassRow, col);
                if (string.IsNullOrWhiteSpace(regClassName) == false)
                {
                    if (regClasses.ContainsKey(regClassName))
                    {
                        colsValid = false;
                        this.LogError($"{sheetName}: Duplicate regulatory class definition detected for: '{regClassName}'.");
                        break;
                    }

                    if (RegClasses.AltNames.All(x => string.Equals(x, regClassName, StringComparison.OrdinalIgnoreCase) == false))
                    {
                        colsValid = false;
                        this.LogError($"{sheetName}: An unknown regulatory class '{regClassName}' was detected.");
                        break;
                    }

                    currKey = regClassName;
                    regClasses.Add(currKey, new List<int>());
                }

                if (regClasses.ContainsKey(currKey))
                {
                    regClasses[currKey].Add(this.GetInt32(modelYearRow, col));
                }
            }

            if (regClasses.Any())
            {
                if (regClasses.Count > RegClasses.AltNames.Length)
                {
                    colsValid = false;
                    this.LogError($"{sheetName}: Too many regulatory classes. Expected [1-{RegClasses.AltNames.Length}].");
                }
                else
                {
                    var modelYears = regClasses.First().Value;
                    if (modelYears.Any() == false || modelYears.First() <= 0)
                    {
                        colsValid = false;
                        this.LogError($"{sheetName}: Regulatory class {regClasses.First().Key} does not contain valid model years.");
                    }
                    else
                    {
                        for (int myIndex = 0; myIndex < modelYears.Count - 1; ++myIndex)
                        {
                            if (modelYears[myIndex + 1] - modelYears[myIndex] != 1)
                            {
                                colsValid = false;
                                this.LogError($"{sheetName}: Model years must increment by 1.");
                                break;
                            }
                        }
                    }

                    if (colsValid && regClasses.Count > 1)
                    {
                        foreach (var regClass in regClasses.Skip(1))
                        {
                            var currModelYears = regClass.Value;
                            if (currModelYears.Count != modelYears.Count)
                            {
                                colsValid = false;
                            }
                            else
                            {
                                for (int i = 0; i < modelYears.Count; ++i)
                                {
                                    if (currModelYears[i] != modelYears[i])
                                    {
                                        colsValid = false;
                                        break;
                                    }
                                }
                            }

                            if (colsValid == false)
                            {
                                this.LogError($"{sheetName}: Model years must be the same for all defined regulatory classes.");
                                break;
                            }
                        }
                    }
                }
            }
            else
            {
                colsValid = false;
                this.LogError($"{sheetName}: There are no regulatory classes defined.");
            }

            if (colsValid)
            {
                minMY = regClasses.First().Value.First();
                maxMY = regClasses.First().Value.Last();
            }

            return colsValid;
        }

        // ----- helper methods -----
        int[] DetectColumnIndexes(int cols, string[] names)
        {
            int[] indexes = Enumerable.Repeat(-1, names.Length).ToArray();
            for (int i = 0; i < cols; i++)
            {
                string column = this.GetString(1, i);
                for (int j = 0; j < names.Length; j++)
                {
                    if (indexes[j] == -1 && StringHelper.Equals(column, names[j], fuzzy: true))
                    {
                        indexes[j] = i;
                        break;
                    }
                }
            }
            return indexes;
        }

        int[] ParseRefreshRedesignYears(string yearsStr)
        {
            List<int> parsedYears = new List<int>();

            // input format using a starting year and interval
            if (yearsStr.Contains('|')) {
                string[] splitStr = yearsStr.Split(new char[] { '|' }, 2);

                int startYear = splitStr[0].GetInt32(defaultValue: 0);
                int interval   = splitStr[1].GetInt32(defaultValue: 0);

                if (startYear > 0 && interval > 0) {
                    // adjust so startYear >= minYear
                    if (startYear < ModelYearKey.MinYear) {
                        int r = (ModelYearKey.MinYear - startYear) % interval == 0 
                            ? 0 
                            : interval - ((ModelYearKey.MinYear - startYear) % interval);
                        startYear = ModelYearKey.MinYear + r;
                    }

                    if (startYear <= ModelYearKey.MaxYear) {
                        int yearCount = ((ModelYearKey.MaxYear - startYear) / interval) + 1;
                        parsedYears = Enumerable.Range(0, yearCount).Select(i => startYear + interval * i).ToList();
                    }
                }
            }
            // input format using delimited list of years
            else {
                string[] years = yearsStr.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
                for (int i = 0; i < years.Length; i++) {
                    int year = years[i].GetInt32();
                    if (!parsedYears.Contains(year) && year >= ModelYearKey.MinYear && year <= ModelYearKey.MaxYear) {
                        parsedYears.Add(year);
                    }
                }
            }

            return parsedYears.ToArray();
        }

        // helper method for validating refresh and redesign years
        void ValidateRefreshRedesignYears(string hdr, string yearsStr, string paramName)
        {
            // input format using a starting year and interval
            if (yearsStr.Contains('|')) {
                string[] splitStr = yearsStr.Split(new char[] { '|' }, 2);

                string yrStr = splitStr[0].Trim();
                string intervalStr = splitStr[1].Trim();

                int yr = yrStr.GetInt32(defaultValue: 0);
                int interval = splitStr[1].GetInt32(defaultValue: 0);

                // We do allow start year outside of range, but invalid years in sequence will not be parsed.
                if (!yrStr.IsInteger() || yr < 1) {
                    this.LogError($"{hdr}Start Year-{yrStr} in {paramName} is not valid (or outside of " +
                        $"valid range).");
                }
                if (!intervalStr.IsInteger() || interval < 1) {
                    this.LogError($"{hdr}Interval-{intervalStr} in {paramName} is not valid (or outside of " +
                        $"valid range.");
                }
            }
            // input format using delimited list of years
            else {
                string[] years = yearsStr.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string year in years) {
                    int yr = year.GetInt32();
                    if (!year.IsInteger() || yr < ModelYearKey.MinYear || yr > ModelYearKey.MaxYear) {
                        this.LogError($"{hdr}MY-{year} in {paramName} is not valid (or outside of valid range).");
                    }
                }
            }
        }

        void ParseComponentRefreshRedesignYears(
            List<BaselineVehicle> bVehs, List<Dictionary<string, object>> bComponentInfoDict,
            string componentType, Func<BaselineVehicle, int> getComponentCode)
        {
            foreach (var item in bComponentInfoDict) {
                int code = (int)item["Code"];
                string refreshYrs = (string)item["RefreshYears"];
                string redesignYrs = (string)item["RedesignYears"];
                bool refreshYrsAuto = StringHelper.Equals(refreshYrs, "AUTO");
                bool redesignYrsAuto = StringHelper.Equals(redesignYrs, "AUTO");

                if (refreshYrsAuto ^ redesignYrsAuto) {
                    this.LogError($"{componentType} (Code:{code}):  " +
                        $"Either both \"Refresh Years\" and \"Redesign Years\" must be \"auto\" or neither.");
                }

                BaselineVehicle leader = null;
                if (refreshYrsAuto || redesignYrsAuto) {
                    var queryVehs = bVehs.Where(veh => getComponentCode(veh) == code);
                    leader = LeaderSelection.SelectLeaderVehicle(queryVehs);
                }

                ModelYearKey parsedRefreshYears = ModelYearKey.Empty;
                if (refreshYrsAuto && leader != null) {
                    parsedRefreshYears = leader.RefreshYears;
                }
                else if (!refreshYrsAuto && refreshYrs.Length != 0) {
                    parsedRefreshYears = ModelYearKey.BuildKey(this.ParseRefreshRedesignYears(refreshYrs));
                }

                ModelYearKey parsedRedesignYears = ModelYearKey.Empty;
                if (redesignYrsAuto && leader != null) {
                    parsedRedesignYears = leader.RedesignYears;
                }
                else if (!redesignYrsAuto && redesignYrs.Length != 0) {
                    parsedRedesignYears = ModelYearKey.BuildKey(this.ParseRefreshRedesignYears(redesignYrs));
                }

                item["LeaderCode"] = (leader == null) ? -1 : leader.Code;
                item["RefreshYears"] = parsedRefreshYears;
                item["RedesignYears"] = parsedRedesignYears;
            }
        }

        void ValidateComponentRefreshRedesignYears(string hdr, string yearsStr, string paramName)
        {
            if (StringHelper.Equals(yearsStr, "AUTO")) {
                return; 
            }
            this.ValidateRefreshRedesignYears(hdr, yearsStr, paramName);
        }

        void LoadTechApplicability(int row, string[] namesTech, int[] indexesTech,
            out TechKey usedTechnologies, out TechKey availableTechnologies)
        {
            usedTechnologies = TechKey.Empty;
            availableTechnologies = TechKey.Empty;

            for (int i = 0; i < namesTech.Length; i++)
            {
                int techIndex = TI.GetIndex(namesTech[i]);
                int techApplicability = this.ParseTechApplicability(this.GetString(row, indexesTech[i]));

                if (techApplicability == 1)
                {
                    usedTechnologies[techIndex] = true;
                }
                else if (techApplicability == 0)
                {
                    availableTechnologies[techIndex] = true;
                }
            }
        }
        int ParseTechApplicability(string value)
        {
            return (value == "SKIP" || value == "_SKIP") ? -1 : (value == "USED" || value == "_USED") ? 1 : 0;
        }

        void LogErrorNotSpecifiedOrNotValid(string hdr, string paramName)
        {
            this.LogError($"{hdr}{paramName} is not specified or not valid.");
        }
        void LogErrorNotValid(string hdr, string paramName)
        {
            this.LogError($"{hdr}{paramName} is not valid.");
        }

        #endregion

        #region /*** Variables ***/

        private static string[] TrueStrings = { "Y", "YES", "T", "TRUE" };
        private static string[] FalseStrings = { "N", "NO", "F", "FALSE" };
        private static string[] ValidRcStrings = { "DC", "IC", "LT", "2B3", "LT2B3", "HDPUV", "" };

        internal IIndustry _ind;

        #endregion

    }
}
