#region << Using Directives >>
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.Utils;

using TC = Volpe.Cafe.TechnologyClasses;
using TI = Volpe.Cafe.TechnologyIndexes;
#endregion

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

        #region /*** Nested Types ***/

        // simple object to track and align year stats accross cost sheets
        struct CostStats { public int MinYear, MaxYear, YearCount; }

        class TechInfo
        {
            public TechInfo()
            {
                this.Attributes = new TechnologyAttributes[TC.List.Length];
                this.VehCosts = new TechnologyCosts[TC.List.Length];
                this.EngCosts = new TechnologyCosts[TC.EngineList.Length];
            }

            public TechnologyAttributes[] Attributes;
            public TechnologyCosts[] VehCosts;
            public TechnologyCosts[] EngCosts;
        }

        #endregion

        #region /*** Ctors ***/

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

        #endregion

        #region /*** Methods ***/

        protected override void ParseInternal()
        {
            // begin parsing ...
            string[] sheets      = this.SheetNames;
            string[] names       = new string[] { "Parameters", "Technologies" };
            int   [] indexes     = new int   [] { -1, -1 };
            int   [] attrIndexes = new int   [TC.List.Length];
            int   [] costIndexes = new int   [TC.EngineList.Length];
            for (int i = 0; i < attrIndexes.Length; i++) { attrIndexes[i] = -1; }
            for (int i = 0; i < costIndexes.Length; i++) { costIndexes[i] = -1; }

            // scan each worksheet name, looking for a match
            for (int i = 0; i < sheets.Length; i++) {
                string sheet = sheets[i].Trim().ToUpper();
                for (int j = 0; j < names.Length; j++) {
                    if (StringHelper.Equals(sheet, names[j], fuzzy: true)) {
                        indexes[j] = i;
                        break;
                    }
                }
                for (int j = 0; j < TC.List.Length; j++) {
                    if (StringHelper.Equals(sheet, TC.Names[j], fuzzy: true)) {
                        attrIndexes[j] = i;
                        break;
                    }
                }
                for (int j = 0; j < TC.EngineList.Length; j++) {
                    if (StringHelper.Equals(sheet, TC.EngineNames[j], fuzzy: true)) {
                        costIndexes[j] = i;
                        break;
                    }
                }
            }

            // check if all sheets were found
            if (!this.VerifyIndexes("Technologies workbook", attrIndexes, TC.      Names, 0, attrIndexes.Length) ||
                !this.VerifyIndexes("Technologies workbook", costIndexes, TC.EngineNames, 0, costIndexes.Length) ||
                !this.VerifyIndexes("Technologies workbook",     indexes,          names, 0,     indexes.Length))
            {
                return;
            }

            //-------------------------------------------//
            // nothing is missing--proceed to parse data //
            //-------------------------------------------//
            // attrs/costs indexed as: tech-index, tech-/eng-class
            TechInfo[] techInfo = Enumerable.Range(0, TI.TechnologyCount).Select(_ => new TechInfo()).ToArray();

            // we need to load tech attributes and costs first, so that we can have tech-info for
            // each veh-/eng-class, from which to build each tech object
            this._minYear = this._maxYear = -1;
            for (int i = 0; i < TC.List.Length; i++) {
                this.LoadTechnologyAttributes(attrIndexes[i], i, techInfo);
                this.LoadTechnologyCosts(attrIndexes[i], i, techInfo, false);
            }
            for (int i = 0; i < TC.EngineList.Length; i++) {
                this.LoadTechnologyCosts(costIndexes[i], i, techInfo, true);
            }

            // load global params & the main technology definitions worksheet
            var globalParams = this.LoadGlobalParameters(indexes[0]);
            var techList = this.LoadTechnologies(indexes[1], techInfo);

            this._techs = new Technologies(new Dictionary<string, object> {
                ["TechnologyList"] = techList,
                ["GliderShare"] = globalParams["GliderShare"]
            });
        }

        void LoadTechnologyAttributes(int wsIndex, int techClass, TechInfo[] techInfo)
        {
            this.ActivateWorksheet(wsIndex);
            int rows = this.Rows;
            int cols = this.Columns;

            string[] names = new string[] { "Name",
                "Applicable", "Year Avail.", "Year Retired",
                "Secondary FS", "Electric Range", "Electric Power",
                "Delta Weight (%)", "Delta Weight (lbs)", "Consumer's Willingness to Pay" };
            int[] indexes = new int[names.Length];
            for (int i = 0; i < indexes.Length; i++) {
                indexes[i] = -1;
            }

            //----- verification -----
            // scan each column to find the appropriate column names
            for (int i = 0; i < cols; i++) {
                string column = this.GetString(1, i);
                for (int j = 0; j < names.Length; j++) {
                    if (StringHelper.Equals(column, names[j], fuzzy: true)) {
                        indexes[j] = i;
                        break;
                    }
                }
            }

            // check if all columns were found
            if (!this.VerifyIndexes(this.SheetNames[wsIndex] + " worksheet", indexes, names)) {
                return;
            }

            //----- load data -----
            // skip the first two rows, which are headers
            for (int i = 2; i < rows; i++) {
                string name = this.GetString(i, indexes[0]);
                int index = TI.GetIndex(name); 

                if (index == -1) {
                    this.LogError($"On the {this.SheetNames[wsIndex]} worksheet, the technology "
                                + $"with the name \"{name}\" is undefined.");
                    continue;
                }

                // parse technology attributes
                bool   applicable     = this.GetBool  (i, indexes[ 1]);
                int    yearAvailable  = this.GetInt32 (i, indexes[ 2]);
                int    yearRetired    = this.GetInt32 (i, indexes[ 3]);
                double secondaryFS    = this.GetDouble(i, indexes[ 4]);
                double electricRange  = this.GetDouble(i, indexes[ 5]);
                double electricPower  = this.GetDouble(i, indexes[ 6]);
                double deltaWeightPCT = this.GetDouble(i, indexes[ 7]);
                double deltaWeightLBS = this.GetDouble(i, indexes[ 8]);
                double consumerWTP    = this.GetDouble(i, indexes[ 9]);

                var attributes = new TechnologyAttributes(new Dictionary<string, object>
                {
                    // tech availability 
                    {"Applicable"       , applicable    },
                    {"YearAvailable"    , yearAvailable },
                    {"YearRetired"      , yearRetired   },

                    // tech attributes
                    {"SecondaryFS"      , secondaryFS   },
                    {"ElectricRange"    , electricRange },
                    {"ElectricPower"    , electricPower },
                    {"DeltaWeightPCT"   , deltaWeightPCT},
                    {"DeltaWeightLBS"   , deltaWeightLBS},
                    {"ConsumerWTP"      , consumerWTP   }
                });

                techInfo[index].Attributes[techClass] = attributes;
            }
        }

        void LoadTechnologyCosts(int wsIndex, int techClass, TechInfo[] techInfo, bool isEngClass)
        {
            this.ActivateWorksheet(wsIndex);
            int rows = this.Rows;
            int cols = this.Columns;

            string[] namesMain = new string[] { "Name" };
            int[] indexesMain = new int[] { -1 };

            string[] namesCost = new string[] {
                "Cost Table",
                "Maintenance and Repair Cost Table",
                "Battery Cost Learning Rates Table"
            };
            int[] indexesCost = new int[] { -1, -1, -1 };
            bool[] optionalCost = new bool[] { false, false, isEngClass }; // battery costs are n/a for eng-cost-classes

            // total number cols in various "cost" tables
            CostStats costTableStats   = new CostStats();
            CostStats mrTableStats     = new CostStats();
            CostStats batLrnTableStats = new CostStats();

            //----- verification -----
            string ws = this.SheetNames[wsIndex];

            void FindCostTableStats(int tableIndex, string prefix, ref int col, ref CostStats stats)
            {
                string errHdr = $"On the '{ws}' worksheet: ";
                string column = this.GetString(1, col);

                if (indexesCost[tableIndex] != -1 ||
                        !StringHelper.StartsWith(column, prefix) ||
                        !StringHelper.Equals(this.GetString(0, col), namesCost[tableIndex])) {
                    return;
                }

                // scan columns to find range of model years
                indexesCost[tableIndex] = col;
                while (StringHelper.StartsWith(column, prefix, fuzzy: true)) {
                    if (!int.TryParse(column.Substring(prefix.Length), out int year) || !ModelYear.IsValid(year)) {
                        this.LogError($"{errHdr}'{column}' value in {namesCost[tableIndex]} could " +
                                      "not be parsed as a valid model year.");
                        return;
                    }
                    else if (stats.MinYear == 0) {
                        stats.MinYear = year;
                    }
                    else if (year != stats.MinYear + stats.YearCount) {
                        this.LogError($"{errHdr}model years in {namesCost[tableIndex]} are not contiguous.");
                        return;
                    }
                    else {
                        stats.MaxYear = year;
                    }

                    stats.YearCount++;
                    col++;
                    if (col == cols) {
                        break;
                    }
                    column = this.GetString(1, col);
                }
                col--;
            }

            // scan each column to find the appropriate column names
            for (int i = 0; i < cols; i++)
            {
                FindCostTableStats(0, "C-", ref i, ref costTableStats);
                FindCostTableStats(1, "M/R-", ref i, ref mrTableStats);
                FindCostTableStats(2, "BCL-", ref i, ref batLrnTableStats);

                // look for all other columns
                string column = this.GetString(1, i);
                for (int j = 0; j < namesMain.Length; j++)
                {
                    if (StringHelper.Equals(column, namesMain[j], fuzzy: true)) {
                        indexesMain[j] = i;
                        break;
                    }
                }
            }

            // check if all columns were found
            if (!this.VerifyIndexes(ws + " worksheet", indexesCost, namesCost, optionalCost)) {
                return;
            }

            // make sure min/max years match for all tables
            if ((costTableStats.MinYear != mrTableStats.MinYear)
                    || (costTableStats.MinYear != batLrnTableStats.MinYear && !isEngClass)) {
                this.LogError($"On the {ws} worksheet, the minimum model year does not match between varying cost tables.");
            }
            if ((costTableStats.MaxYear != mrTableStats.MaxYear)
                    || (costTableStats.MaxYear != batLrnTableStats.MaxYear && !isEngClass)) {
                this.LogError($"On the {ws} worksheet, the maximum model year does not match between varying cost tables.");
            }

            // at this point, if parsing errors were encountered, stop trying to load file and return ...
            if (this.HasErrors) {
                return;
            }

            // compare min/max years accross all cost sheets
            if (this._minYear == -1)
            {   // save min/max years from first parsed sheet
                this._minYear = costTableStats.MinYear;
                this._maxYear = costTableStats.MaxYear;
            }
            else if (this._minYear != costTableStats.MinYear || this._maxYear != costTableStats.MaxYear)
            {
                this.LogError($"On the {ws} worksheet, the range of covered model years does not match previous worksheet(s).");
                return;
            }

            //----- load data -----
            // skip the first two rows, which are headers
            for (int i = 2; i < rows; i++)
            {
                string name = this.GetString(i, indexesMain[0]);
                int index = TI.GetIndex(name);

                if (index == -1) {
                    this.LogError($"On the {this.SheetNames[wsIndex]} worksheet, the technology "
                                + $"with the name \"{name}\" is undefined.");
                    continue;
                }

                // parse cost tables (tech cost, maint/repair, and bat.lrn rates)
                double[] costTable                = new double[costTableStats.YearCount];
                double[] maintRepairCostTable     = new double[costTableStats.YearCount];
                double[] batteryLearningRateTable = new double[costTableStats.YearCount];

                for (int j = 0; j < costTableStats.YearCount; j++) {
                    costTable[j] = this.GetDouble(i, indexesCost[0] + j);
                }
                for (int j = 0; j < costTableStats.YearCount; j++) {
                    maintRepairCostTable[j] = this.GetDouble(i, indexesCost[1] + j);
                }
                if (!isEngClass) {
                    for (int j = 0; j < costTableStats.YearCount; j++) {
                        batteryLearningRateTable[j] = this.GetDouble(i, indexesCost[2] + j);
                    }
                }

                var costs = new TechnologyCosts(new Dictionary<string, object>
                {
                    {"MinMY", costTableStats.MinYear},
                    {"MaxMY", costTableStats.MaxYear},
                    {"CostTable", costTable},
                    {"MaintRepairCostTable", maintRepairCostTable},
                    {"BatteryLearningRateTable", batteryLearningRateTable}
                });

                if (isEngClass) {
                    techInfo[index].EngCosts[techClass] = costs;
                }
                else {
                    techInfo[index].VehCosts[techClass] = costs;
                }
            }
        }

        Dictionary<string, object> LoadGlobalParameters(int wsIndex)
        {
            this.ActivateWorksheet(wsIndex);
            int rows = this.Rows;
            int cols = this.Columns;

            // check class-specific values
            string[] names   = { "Tech Class", "Glider Share" };
            int   [] indexes = { -1, -1 };

            // scan each column to find the appropriate column names
            for (int i = 0; i < cols; i++)
            {
                string column = this.GetString(3, i);
                for (int j = 0; j < names.Length; j++)
                {
                    if (StringHelper.Equals(column, names[j], fuzzy: true)) { indexes[j] = i; break; }
                }
            }
            // check if all columns were found
            if (!this.VerifyIndexes(this.SheetNames[wsIndex] + " worksheet", indexes, names)) { return null; }
            // ensure that "Tech Class" column is always left-most
            if (indexes[0] != 0)
            {
                this.LogError($"On the {this.SheetNames[wsIndex]} worksheet, the \"Tech Class\" column must be left-most.");
                return null;
            }

            // check if all technology classes are defined and in correct order
            int[] techClassIndexes = new int[TC.List.Length];
            for (int i = 0; i < techClassIndexes.Length; i++) { techClassIndexes[i] = -1; }
            //
            for (int i = 4; i < rows; i++)
            {
                string className = this.GetString(i, 0);
                if (className.Length == 0) { break; } // do not allow gaps (empty rows) between tech-classes
                for (int j = 0; j < TC.Names.Length; j++)
                {
                    if (StringHelper.Equals(className, TC.Names[j], fuzzy: true)) { techClassIndexes[j] = i; break; }
                }
            }
            // check if all tech-classes were found
            if (!this.VerifyIndexes(this.SheetNames[wsIndex] + " worksheet", techClassIndexes, TC.Names)) {
                return null;
            }

            // once all checks passed, load the class-specific parameters
            double[] gliderShare = new double[TC.List.Length];
            for (int i = 0; i < TC.List.Length; i++)
            {
                gliderShare[i] = this.GetDouble(techClassIndexes[i], indexes[1]);
            }

            return new Dictionary<string, object> {
                ["GliderShare"] = new ReadOnlyCollection<double>(gliderShare)
            };
        }

        IReadOnlyList<TechnologyInfo> LoadTechnologies(int wsIndex, TechInfo[] techInfo)
        {
            this.ActivateWorksheet(wsIndex);
            int rows = this.Rows;
            int cols = this.Columns;
            var techList = new TechnologyInfo[TI.TechnologyCount];

            string[] names = new string[] {
                "Name", "Phase-in Cap", "Phase-in Start Year",
                "ZEV Credits (2025 and Earlier)", "ZEV Credits (2026 and Later)", "HDPUV ZEV Credits"
            };
            int[] indexes = Enumerable.Repeat(-1, names.Length).ToArray();

            //----- verification -----
            // scan each column to find the appropriate column names
            for (int i = 0; i < cols; i++) {
                string column = this.GetString(1, i);
                for (int j = 0; j < names.Length; j++) {
                    if (StringHelper.Equals(column, names[j], fuzzy: true)) {
                        indexes[j] = i; break;
                    }
                }
            }

            // check if all columns were found
            if (!this.VerifyIndexes(this.SheetNames[wsIndex] + " worksheet", indexes, names)) {
                return techList;
            }

            //----- load data -----
            for (int i = 2; i < rows; i++)
            {
                string name = this.GetString(i, indexes[0]);
                int techIndex = TI.GetIndex(name);

                if (techIndex == -1) {
                    this.LogError("On the main Technologies worksheet, the technology with "
                                + $"name \"{name}\" is undefined.");
                    continue;
                }

                // parse global technology attributes
                double phaseInCap       = this.GetDouble(i, indexes[1]);
                int    phaseInStartYear = this.GetInt32 (i, indexes[2]);
                double zevCreditsMY2025 = this.GetDouble(i, indexes[3]);
                double zevCreditsMY2026 = this.GetDouble(i, indexes[4]);
                double zevCreditsHDPUV  = this.GetDouble(i, indexes[5]);

                // save technology at a specific index
                var values = new Dictionary<string, object> {
                    // general tech info
                    ["Name"] = name,
                    
                    // phase-in info
                    ["PhaseInCap"] = phaseInCap,
                    ["PhaseInStartYear"] = phaseInStartYear,

                    // zev credits
                    ["ZevCreditsMY2025"] = zevCreditsMY2025,
                    ["ZevCreditsMY2026"] = zevCreditsMY2026,
                    ["ZevCreditsHDPUV"] = zevCreditsHDPUV,

                    // attributes and costs
                    ["Attributes"] = techInfo[techIndex].Attributes,
                    ["VehicleCosts"] = techInfo[techIndex].VehCosts,
                    ["EngineCosts"] = techInfo[techIndex].EngCosts
                };

                techList[techIndex] = new TechnologyInfo(values);
            }

            // verify that all of the technologies defined in the model were loaded from the input file
            for (int i = 0; i < techList.Length; i++) {
                if (techList[i] == null) {
                    string techName = TI.GetName(i);
                    this.LogError("On the main Technologies worksheet, missing technology "
                                + $"definition for \"{techName}\".");
                }
            }

            return new ReadOnlyCollection<TechnologyInfo>(techList);
        }

        #endregion

        #region /*** Variables ***/

        internal Technologies _techs;
        int _minYear, _maxYear;

        #endregion

    }
}
