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

using Volpe.Cafe.Generic;
using Volpe.Cafe.MarketData;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.Model;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.Utils;

namespace Volpe.Cafe.Reporting.Generators
{
    /// <summary>
    /// Defines an enumerable object used for generating rows of data for a compliance report.
    /// </summary>
    [Serializable]
    public class ComplianceReportDataGenerator : ReportDataGenerator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ComplianceReportDataGenerator"/> class, for the specified
        /// scenario and reporting data.
        /// </summary>
        /// <inheritdoc cref="ReportDataGenerator(Scenario, ReportingData, ModelingSettings)" path="/param"/>
        public ComplianceReportDataGenerator(Scenario scen, ReportingData data, ModelingSettings settings)
            : base(scen, data, settings)
        {
            int mfrCount = this.Data.ScenarioData.ManufacturerCount;
            this._mfrRange = new Range(0, mfrCount - 1, true);
        }

        private const int NumCostValues = 16;

        private Range _mfrRange;
        private ComplianceReportData _scenData;
        private ComplianceReportData _baseData;

        /// <inheritdoc/>
        protected override bool WriteYearTotals => false;

        /// <inheritdoc/>
        protected override IEnumerator<object[]> GetYearEnumerator(int? year)
        {
            foreach (int? mfr in this._mfrRange) {
                this.InitDataObjects(year.Value, mfr);

                for (int rc = 0; rc < RegClasses.ReportingList.Length; rc++) {
                    if (this.Data.DataFilters.ReportRC[rc]) {
                        yield return this.GetValues(year, mfr, rc);
                    }
                }
            }
        }

        private void InitDataObjects(int year, int? mfr)
        {
            var scenData = this.Data.ScenarioData;
            this._scenData = (mfr == null)
                ? new ComplianceReportData(year, scenData, this.Settings)
                : new ComplianceReportData(year, scenData.Manufacturers[mfr.Value], this.Settings);

            if (!this.Scenario.IsBaseline) {
                var baseData = this.Data.BaselineData;
                this._baseData = (mfr == null)
                    ? new ComplianceReportData(year, baseData, this.Settings)
                    : new ComplianceReportData(year, baseData.Manufacturers[mfr.Value], this.Settings);
            }
        }

        private object[] GetValues(int? year, int? mfr, int rc)
        {
            this.ClearBuffer();

            var regClass = RegClasses.ReportingList[rc];

            this.AppendHeader(year, mfr, rc);
            this.AppendSalesAndEmployement(regClass);
            this.AppendComplianceStates(year, regClass);
            this.AppendCafeComplianceMetrics(year, regClass);
            this.AppendCafeCreditsAndAdjustments(year, mfr, regClass);
            this.AppendCO2ComplianceMetrics(year, regClass);
            this.AppendCO2CreditsAndAdjustments(year, mfr, regClass);
            this.AppendVehAttributes(year, regClass);
            this.AppendZevCreditInfo(year, regClass);
            this.AppendCostTotalsAndAverages(regClass);
            this.AppendCafeCreditInfo(regClass);
            this.AppendCO2CreditInfo(regClass);

            return this.GetBuffer();
        }

        private void AppendHeader(int? year, int? mfr, int rc)
        {
            this.AppendBuffer(this.Scenario.Index);
            this.AppendBuffer(StringHelper.GetTitleCase(this.Scenario.Description));
            this.AppendBuffer(year == null ? "TOTAL" : year.ToString());
            this.AppendBuffer(mfr == null ? "TOTAL" : this._scenData.Manufacturer.ProperName);
            this.AppendBuffer(RegClasses.ReportingNames[rc]);
        }

        private void AppendSalesAndEmployement(RegulatoryClass regClass)
        {
            this.AppendBuffer(this._scenData.Sales[regClass]);
            this.AppendCostValue(this._scenData.Jobs[regClass]);
        }

        private void AppendComplianceStates(int? year, RegulatoryClass regClass)
        {
            var mfr = this._scenData.Manufacturer;
            if (mfr == null || year == null) {
                this.AppendEmpty(3);
                return;
            }

            this.AppendBuffer(GetIsInCompliance(ComplianceProgram.CAFE, RegClasses.List));

            if (!regClass.IsDC() && !regClass.IsIC()) {
                this.AppendBuffer(GetIsInCompliance(ComplianceProgram.CO2, RegClasses.AltList));
            }
            else {
                this.AppendEmpty(1);
            }

            if (regClass.IsHDPUV() || regClass == RegulatoryClass.AllLightDuty || regClass == RegulatoryClass.All) {
                var zevCompliance = Standards.CalcZevCompliance(this.Scenario, year.Value, mfr, regClass,
                                                                this.Settings);
                this.AppendBuffer(zevCompliance.Target <= zevCompliance.Credits ? "Yes" : "No");
            }
            else {
                this.AppendEmpty(1);
            }

            string GetIsInCompliance(ComplianceProgram program, RegulatoryClass[] regClasses)
            {
                bool isInCompliance =
                    regClasses.All(rc => !regClass.Contains(rc) || mfr.IsInCompliance(program, rc, year));

                return isInCompliance ? "Yes" : "No";
            }
        }

        private void AppendCafeComplianceMetrics(int? year, RegulatoryClass regClass)
        {
            if (year == null) {
                this.AppendEmpty(7);
                return;
            }

            var sales = this._scenData.Sales;
            var stats = this._scenData.ComplianceStats;

            double mdpcStandard = (sales[regClass] == 0) ? 0 : this._scenData.Industry.MdpcStandard[year.Value];
            double pStandard = stats.PStandard.CalcHarmonicMean(regClass, sales);
            double standard = stats.Standard.CalcHarmonicMean(regClass, sales);
            double cafe2Cycle = stats.CAFE2Cycle.CalcHarmonicMean(regClass, sales);
            double cafeRating = stats.CAFERating.CalcHarmonicMean(regClass, sales);

            this.AppendBuffer(Math.Round(pStandard, 1));
            if (regClass.IsDC()) {
                this.AppendBuffer(Math.Round(mdpcStandard, 1));
            }
            else {
                this.AppendEmpty(1);
            }
            this.AppendBuffer(Math.Round(standard, 1));
            this.AppendBuffer(Math.Round(100 / standard, 3));
            this.AppendBuffer(Math.Round(cafe2Cycle, 1));
            this.AppendBuffer(Math.Round(cafeRating, 1));
            this.AppendBuffer(Math.Round(100 / cafeRating, 3));
        }
        private void AppendCafeCreditsAndAdjustments(int? year, int? mfr, RegulatoryClass regClass)
        {
            if (year == null || mfr == null) {
                this.AppendEmpty(3);
                return;
            }

            var sales = this._scenData.Sales;
            if (sales[regClass] == 0) {
                this.AppendBuffer("0", 3);
            }
            else {
                var stats = this._scenData.ComplianceStats;
                double acEfficiency = stats.AcEfficiency.CalcArithmeticMean(regClass, sales);
                double offCycle = stats.OffCycle.CalcArithmeticMean(regClass, sales);
                double fspCredits = stats.FSPCredits.CalcArithmeticMean(regClass, sales);

                this.AppendBuffer(Math.Round(acEfficiency, 3));
                this.AppendBuffer(Math.Round(offCycle, 3));
                this.AppendBuffer(Math.Round(fspCredits, 3));
            }
        }

        private void AppendCO2ComplianceMetrics(int? year, RegulatoryClass regClass)
        {
            if (year == null || regClass.IsDC() || regClass.IsIC()) {
                this.AppendEmpty(2);
                return;
            }

            var sales = this._scenData.SalesAlt;
            var stats = this._scenData.ComplianceStats;

            double co2Standard = stats.CO2Standard.CalcArithmeticMean(regClass, sales);
            double co2Rating = stats.CO2Rating.CalcArithmeticMean(regClass, sales);

            this.AppendBuffer(Math.Round(co2Standard));
            this.AppendBuffer(Math.Round(co2Rating));
        }
        private void AppendCO2CreditsAndAdjustments(int? year, int? mfr, RegulatoryClass regClass)
        {
            if (year == null || mfr == null || regClass.IsDC() || regClass.IsIC()) {
                this.AppendEmpty(4);
                return;
            }

            var sales = this._scenData.SalesAlt;
            if (sales[regClass] == 0) {
                this.AppendBuffer("0", 4);
            }
            else {
                var stats = this._scenData.ComplianceStats;
                double co2AcEfficiency = stats.CO2AcEfficiency.CalcArithmeticMean(regClass, sales);
                double co2AcLeakage = stats.CO2AcLeakage.CalcArithmeticMean(regClass, sales);
                double co2OffCycle = stats.CO2OffCycle.CalcArithmeticMean(regClass, sales);
                double co2FspCredits = stats.CO2FSPCredits.CalcArithmeticMean(regClass, sales);

                this.AppendBuffer(Math.Round(co2AcEfficiency, 3));
                this.AppendBuffer(Math.Round(co2AcLeakage, 3));
                this.AppendBuffer(Math.Round(co2OffCycle, 3));
                this.AppendBuffer(Math.Round(co2FspCredits, 3));
            }
        }

        private void AppendVehAttributes(int? year, RegulatoryClass regClass)
        {
            if (year == null) {
                this.AppendEmpty(3);
                return;
            }

            // calc averages
            var scenVehs = this._scenData.Vehicles;

            double sales = 0;
            double avgCW = 0;
            double avgFP = 0;
            double avgWF = 0;

            foreach (IVehicle veh in scenVehs) {
                var vehRevision = veh.GetRevision(year);
                double vehSales = vehRevision.Sales;
                if (regClass.Contains(vehRevision.RegClass)) {
                    sales += vehSales;
                    avgCW += vehSales * Math.Round(vehRevision.CurbWeight);
                    avgFP += vehSales * Math.Round(vehRevision.Footprint, 1);
                    avgWF += vehSales * vehRevision.GetWorkFactor();
                }
            }

            if (sales != 0) {
                avgCW /= sales;
                avgFP /= sales;
                avgWF /= sales;
            }

            this.AppendWeightValue(avgCW);
            this.AppendFootprintValue(avgFP);
            this.AppendWeightValue(avgWF);
        }

        private void AppendZevCreditInfo(int? year, RegulatoryClass regClass)
        {
            if (year == null || !(regClass == RegulatoryClass.All
                    || regClass == RegulatoryClass.AllLightDuty || regClass.IsHDPUV())) {
                this.AppendEmpty(2);
                return;
            }

            var scen = this.Scenario;
            var industry = this._scenData.Industry;
            var mfr = this._scenData.Manufacturer;
            var settings = this.Settings;

            ZevCompliance zevCompliance = (mfr == null)
                ? Standards.CalcZevCompliance(scen, year.Value, industry, regClass, settings)
                : Standards.CalcZevCompliance(scen, year.Value, mfr, regClass, settings);

            this.AppendBuffer(Math.Round(zevCompliance.Target));
            this.AppendBuffer(Math.Round(zevCompliance.Credits));
        }

        private void AppendCostTotalsAndAverages(RegulatoryClass regClass)
        {
            // NOTE:
            //  - for the baseline scenario all costs are written as absolutes
            //  - for alternative scenarios:
            //      - for "incremental" reporting, the costs are written as incremental over the baseline
            //      - for "absolute" reporting, the costs are written as absolute

            bool isBaseline  = this.Scenario.IsBaseline;
            double scenSales = this._scenData.Sales[regClass];
            double baseSales = isBaseline ? scenSales : this._baseData.Sales[regClass];

            if (scenSales == 0 && baseSales == 0) {
                this.AppendBuffer(0, NumCostValues * 2);
                return;
            }

            double[] scenCosts = this._scenData.GetArrayOfCosts(regClass);

            if (isBaseline || !this.Flags.HasIncremental() || baseSales == 0) {
                // append cost totals
                for (int i = 0; i < scenCosts.Length; i++) {
                    this.AppendCostValue(scenCosts[i]);
                }

                // append cost averages
                for (int i = 0; i < scenCosts.Length; i++) {
                    double avgScenCosts = (scenSales == 0) ? 0 : scenCosts[i] / scenSales;
                    this.AppendCostValue(avgScenCosts);
                }
            }
            else {
                double[] baseCosts = this._baseData.GetArrayOfCosts(regClass);

                // append cost totals
                for (int i = 0; i < scenCosts.Length; i++) {
                    this.AppendCostValue(scenCosts[i] - baseCosts[i]);
                }

                // append cost averages
                for (int i = 0; i < scenCosts.Length; i++) {
                    double avgScenCosts = (scenSales == 0) ? 0 : scenCosts[i] / scenSales;
                    this.AppendCostValue(avgScenCosts - baseCosts[i] / baseSales);
                }
            }
        }

        private void AppendCafeCreditInfo(RegulatoryClass regClass)
        {
            var stats = this._scenData.ComplianceStats;
            this.AppendBuffer(stats.Credits[regClass]);
            this.AppendBuffer(stats.CreditsOut[regClass]);
            this.AppendBuffer(stats.CreditsIn[regClass]);
        }
        private void AppendCO2CreditInfo(RegulatoryClass regClass)
        {
            if (regClass.IsDC() || regClass.IsIC()) {
                this.AppendEmpty(3);
                return;
            }

            var stats = this._scenData.ComplianceStats;
            this.AppendBuffer(stats.CO2Credits[regClass]);
            this.AppendBuffer(stats.CO2CreditsOut[regClass]);
            this.AppendBuffer(stats.CO2CreditsIn[regClass]);
        }

        [Serializable]
        private sealed class ComplianceReportData
        {
            public ComplianceReportData(int year, IManufacturer mfr, ModelingSettings settings)
            {
                this.Industry = mfr.Industry;
                this.Manufacturer = mfr;
                this.Vehicles = mfr.Vehicles;

                var revisionInfo = mfr.GetRevision(year);
                this.ComplianceStats = revisionInfo.ComplianceStats;
                this.ComplianceCosts = revisionInfo.ComplianceCosts;
                this.InitAuxiliaryHevCosts = revisionInfo.InitAuxiliaryHevCosts;
                this.AuxiliaryHevCosts = revisionInfo.AuxiliaryHevCosts;

                this.Sales = this.ComplianceStats.GetSales();
                this.SalesAlt = (RCAltDouble)this.Sales;
                this.Jobs = this.CalcJobs(year, mfr, settings.Parameters.EmploymentValues);
            }

            public ComplianceReportData(int year, IIndustry industry, ModelingSettings settings)
            {
                this.Industry = industry;
                this.Manufacturer = null;
                this.Vehicles = industry.Vehicles;

                this.AggregateIndustryData(year, industry, settings.Parameters.EmploymentValues);
            }

            public IIndustry Industry { get; }
            public IManufacturer Manufacturer { get; }
            public IReadOnlyList<IVehicle> Vehicles { get; }
            
            public RCDouble Sales { get; private set; }
            public RCAltDouble SalesAlt { get; private set; }
            public RCDouble Jobs { get; private set; }

            public ComplianceStatistics ComplianceStats { get; private set; }
            public RCValue<ComplianceCosts> ComplianceCosts { get; private set; }
            public RCValue<AuxiliaryHEVCosts> InitAuxiliaryHevCosts { get; private set; }
            public RCValue<AuxiliaryHEVCosts> AuxiliaryHevCosts { get; private set; }

            public double[] GetArrayOfCosts(RegulatoryClass regClass)
            {
                var mfrStats = this.ComplianceStats;
                var mfrCosts = this.ComplianceCosts.GetValue(regClass);
                var initAuxCosts = this.InitAuxiliaryHevCosts.GetValue(regClass);
                var auxCosts = this.AuxiliaryHevCosts.GetValue(regClass);

                return new double[] {
                    mfrCosts.ACEfficiencyCost,
                    mfrCosts.ACLeakageCost,
                    mfrCosts.OffCycleCost,
                    mfrCosts.BatteryTechCost,
                    mfrCosts.NonBatteryTechCost,
                    mfrCosts.TotalTechCost,
                    mfrStats.Fines[regClass],
                    mfrCosts.RegCost,
                    mfrCosts.MaintRepairCost,
                    auxCosts.TaxCredit,
                    auxCosts.BatteryTaxCredit,

                    // auxiliary delta HEV Costs
                    auxCosts.HevCost          - initAuxCosts.HevCost,
                    auxCosts.TaxCredit        - initAuxCosts.TaxCredit,
                    auxCosts.BatteryTaxCredit - initAuxCosts.BatteryTaxCredit,
                    auxCosts.ConsumerWTP      - initAuxCosts.ConsumerWTP,
                    auxCosts.TechBurden       - initAuxCosts.TechBurden
                };
            }

            private RCDouble CalcJobs(int year, IManufacturer mfr, EmploymentValues employmentValues)
            {
                RCDouble jobs = mfr.CalcLaborHours(year, employmentValues);
                foreach (var regClass in RegClasses.List) {
                    jobs[regClass] /= employmentValues.AnnualLaborHours;
                }
                return jobs;
            }

            private void AggregateIndustryData(int year, IIndustry industry, EmploymentValues employmentValues)
            {
                this.ComplianceStats = new ComplianceStatistics();
                this.ComplianceCosts = new RCValue<ComplianceCosts>(() => new ComplianceCosts());
                this.InitAuxiliaryHevCosts = new RCValue<AuxiliaryHEVCosts>(() => new AuxiliaryHEVCosts());
                this.AuxiliaryHevCosts = new RCValue<AuxiliaryHEVCosts>(() => new AuxiliaryHEVCosts());

                this.Sales = new RCDouble();
                this.SalesAlt = new RCAltDouble();
                this.Jobs = new RCDouble();

                foreach (var mfr in industry.Manufacturers) {
                    var mfrRevision = mfr.GetRevision(year);
                    var mfrStats = mfrRevision.ComplianceStats;
                    var mfrSales = mfrStats.GetSales();
                    var mfrSalesAlt = (RCAltDouble)mfrSales;

                    // aggregate sales & labor
                    this.Sales.Append(mfrSales);
                    this.SalesAlt.Append(mfrSalesAlt);
                    this.Jobs.Append(this.CalcJobs(year, mfr, employmentValues));

                    // aggregate CAFE compliance stats
                    for (int rc = 0; rc < mfrSales.Items.Length; rc++) {
                        double mfrSalesRC = mfrSales.Items[rc];
                        if (mfrSalesRC == 0) {
                            continue;
                        }

                        this.ComplianceStats.PStandard .Items[rc] += mfrSalesRC / mfrStats.PStandard .Items[rc];
                        this.ComplianceStats.Standard  .Items[rc] += mfrSalesRC / mfrStats.Standard  .Items[rc];
                        this.ComplianceStats.CAFE2Cycle.Items[rc] += mfrSalesRC / mfrStats.CAFE2Cycle.Items[rc];
                        this.ComplianceStats.CAFERating.Items[rc] += mfrSalesRC / mfrStats.CAFERating.Items[rc];

                        this.ComplianceStats.Fines     .Items[rc] += mfrStats.Fines     .Items[rc];
                        this.ComplianceStats.Credits   .Items[rc] += mfrStats.Credits   .Items[rc];
                        this.ComplianceStats.CreditsOut.Items[rc] += mfrStats.CreditsOut.Items[rc];
                        this.ComplianceStats.CreditsIn .Items[rc] += mfrStats.CreditsIn .Items[rc];
                    }

                    // aggregate CO2 compliance stats
                    for (int rc = 0; rc < mfrSalesAlt.Items.Length; rc++) {
                        double mfrSalesAltRC = mfrSalesAlt.Items[rc];
                        if (mfrSalesAltRC == 0) {
                            continue;
                        }

                        this.ComplianceStats.CO2Standard.Items[rc] += mfrSalesAltRC * mfrStats.CO2Standard.Items[rc];
                        this.ComplianceStats.CO2Rating  .Items[rc] += mfrSalesAltRC * mfrStats.CO2Rating  .Items[rc];

                        this.ComplianceStats.CO2Credits   .Items[rc] += mfrStats.CO2Credits   .Items[rc];
                        this.ComplianceStats.CO2CreditsOut.Items[rc] += mfrStats.CO2CreditsOut.Items[rc];
                        this.ComplianceStats.CO2CreditsIn .Items[rc] += mfrStats.CO2CreditsIn .Items[rc];
                    }

                    // aggregate costs
                    this.ComplianceCosts      .Append(mfrRevision.ComplianceCosts);
                    this.InitAuxiliaryHevCosts.Append(mfrRevision.InitAuxiliaryHevCosts);
                    this.AuxiliaryHevCosts    .Append(mfrRevision.AuxiliaryHevCosts);
                }

                // finalize CAFE compliance stats
                for (int i = 0; i < this.Sales.Items.Length; i++) {
                    double salesRC = this.Sales.Items[i];
                    if (salesRC == 0) {
                        continue;
                    }

                    this.ComplianceStats.PStandard .Items[i] = salesRC / this.ComplianceStats.PStandard .Items[i];
                    this.ComplianceStats.Standard  .Items[i] = salesRC / this.ComplianceStats.Standard  .Items[i];
                    this.ComplianceStats.CAFE2Cycle.Items[i] = salesRC / this.ComplianceStats.CAFE2Cycle.Items[i];
                    this.ComplianceStats.CAFERating.Items[i] = salesRC / this.ComplianceStats.CAFERating.Items[i];
                }

                // finalize CO2 compliance stats
                for (int i = 0; i < this.SalesAlt.Items.Length; i++) {
                    double salesRC = this.SalesAlt.Items[i];
                    if (salesRC == 0) {
                        continue;
                    }

                    this.ComplianceStats.CO2Standard.Items[i] =
                        Math.Round(this.ComplianceStats.CO2Standard.Items[i] / salesRC);
                    this.ComplianceStats.CO2Rating.Items[i] =
                        Math.Round(this.ComplianceStats.CO2Rating.Items[i] / salesRC);
                }
            }

        }

    }
}
