using System;
using System.Collections.Generic;

using Volpe.Cafe.EffectsData;
using Volpe.Cafe.EffectsData.Collections;
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 consumer costs report.
    /// </summary>
    [Serializable]
    public class ConsumerCostsReportDataGenerator : ReportDataGenerator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ConsumerCostsReportDataGenerator"/> class, for the specified
        /// scenario and reporting data.
        /// </summary>
        /// <inheritdoc cref="ReportDataGenerator(Scenario, ReportingData, ModelingSettings)" path="/param"/>
        public ConsumerCostsReportDataGenerator(Scenario scen, ReportingData data, ModelingSettings settings)
            : base(scen, data, settings) { }

        private const int NumCostValues = 15;

        private ConsumerCostsCollection _scenData;
        private ConsumerCostsCollection _baseData;

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

        /// <inheritdoc/>
        protected override IEnumerator<object[]> GetYearEnumerator(int? year)
        {
            var scenData = this.Data.ScenarioEffectsData?[year.Value]?.AggregateEffects;
            if (scenData == null || scenData.ConsumerCosts == null || scenData.DiscConsumerCosts == null) {
                yield break;
            }

            int drCount = this.Settings.Parameters.EconomicValues.ConsumerDiscRate.Length;
            for (int dr = -1; dr < drCount; dr++) {
                this.InitDataObjects(year.Value, dr);

                // start at "rc = 2" to skip DC and IC reg-classes
                for (int rc = 2; rc < RegClasses.ReportingList.Length; rc++) {
                    if (this.Data.DataFilters.ReportRC[rc]) {
                        yield return this.GetValues(year, rc);
                    }
                }
            }
        }

        private void InitDataObjects(int year, int dr)
        {
            var scenData = this.Data.ScenarioEffectsData[year].AggregateEffects;
            this._scenData = (dr == -1) ? scenData.ConsumerCosts : scenData.DiscConsumerCosts[dr];

            if (!this.Scenario.IsBaseline) {
                var baseData = this.Data.BaselineEffectsData[year].AggregateEffects;
                this._baseData = (dr == -1) ? baseData.ConsumerCosts : baseData.DiscConsumerCosts[dr];
            }
        }

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

            var regClass = RegClasses.ReportingList[rc];

            this.AppendHeader(year, rc);
            this.AppendPaybacks(year, regClass);
            this.AppendSalesAndCostAverages(regClass);
            this.AppendSubtotals(regClass);

            return this.GetBuffer();
        }

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

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

            var scenData = this._scenData[regClass];
            this.AppendRounded(scenData.Payback);
            this.AppendRounded(scenData.PaybackTCO);
        }

        private void AppendSalesAndCostAverages(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 ignoreBaseData = this.Scenario.IsBaseline || !this.Flags.HasIncremental();
            var scenData = this._scenData[regClass];
            var baseData = ignoreBaseData ? scenData : this._baseData[regClass];

            this.AppendBuffer(scenData.Sales);

            if (scenData.Sales == 0 && baseData.Sales == 0) {
                this.AppendBuffer(0, NumCostValues);
                return;
            }

            double[] scenCosts = GetArrayOfCosts(scenData);
            if (ignoreBaseData || baseData.Sales == 0) {
                for (int i = 0; i < scenCosts.Length; i++) {
                    this.AppendRounded(scenCosts[i]);
                }
            }
            else {
                double[] baseCosts = GetArrayOfCosts(baseData);
                for (int i = 0; i < scenCosts.Length; i++) {
                    double scenCosts_i = (scenData.Sales == 0) ? 0 : scenCosts[i];
                    this.AppendRounded(scenCosts_i - baseCosts[i]);
                }
            }

            // local helper function
            double[] GetArrayOfCosts(ConsumerCosts costs)
            {
                // cost are originally in $k -- convert to $-unit when averaging
                double sales = 1000 / costs.Sales;
                return new double[] {
                    sales * costs.NewSalesSurplus,
                    sales * costs.TechCosts,
                    sales * costs.RegCosts,
                    sales * costs.TaxCredits,
                    sales * costs.BatteryTaxCredits,
                    sales * costs.MaintRepairCosts,
                    sales * costs.OpportunityCost,
                    sales * costs.OwnershipCosts.TaxesAndFees,
                    sales * costs.OwnershipCosts.FinancingCosts,
                    sales * costs.OwnershipCosts.InsuranceCosts,
                    sales * costs.RetailFuelCosts,
                    sales * costs.ReboundFuelCosts,
                    sales * costs.DriveSurplus,
                    sales * costs.ReallocatedVMTSurplus,
                    sales * costs.RefuelSurplus
                };
            }
        }

        private void AppendSubtotals(RegulatoryClass regClass)
        {
            if (this.Scenario.IsBaseline) {
                this.AppendBuffer("0", 3);
                return;
            }

            // these values are inherently incremental, so will be reported as such even in "absolute" reports
            var scenCosts = this._scenData[regClass];
            var baseCosts = this._baseData[regClass];

            double averageCosts = scenCosts.GetAverageConsumerCosts(baseCosts);
            double averageBenefits = scenCosts.GetAverageConsumerBenefits(baseCosts);

            this.AppendRounded(averageCosts * 1000);
            this.AppendRounded(averageBenefits * 1000);
            this.AppendRounded((averageBenefits - averageCosts) * 1000);
        }

    }
}
