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

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

        private SocietalCostsCollection _scenData;
        private SocietalCostsCollection _baseData;

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

        /// <inheritdoc/>
        protected override Range CreateYearRange()
        {
            int minYear = Math.Max(this.Data.MinEffectsYear, this.Data.MinYear - Globals.CY + 1);
            return new Range(minYear, this.Data.MaxYear, this.WriteYearTotals);
        }

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

            int drCount = this.Settings.Parameters.EconomicValues.DiscountRate.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.SocialCosts : scenData.DiscSocialCosts[dr];

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

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

            var regClass = RegClasses.ReportingList[rc];
            var scenData = this._scenData[regClass, FuelType.All];
            var baseData = this.Scenario.IsBaseline ? scenData : this._baseData[regClass, FuelType.All];

            this.AppendHeader(year, rc);
            this.AppendEconAndSafetyCosts(scenData, baseData);
            this.AppendEmissionCosts(scenData, baseData);
            this.AppendSubtotals(scenData, baseData);

            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 AppendEconAndSafetyCosts(SocietalCosts scenData, SocietalCosts baseData)
        {
            // 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();
            double[] scenCosts = GetArrayOfCosts(scenData);
            double[] baseCosts = ignoreBaseData ? null : GetArrayOfCosts(baseData);

            this.AppendArrayOfValues(scenCosts, baseCosts);

            // local helper function
            double[] GetArrayOfCosts(SocietalCosts costs)
            {
                return new double[] {
                    // tech, usage, and energy costs
                    costs.NewSalesSurplus,
                    costs.TechCosts,
                    costs.MaintRepairCosts,
                    costs.OpportunityCost,
                    costs.CommercialOppCost,
                    costs.FuelTaxCosts,
                    costs.RetailFuelCosts,
                    costs.DriveSurplus,
                    costs.RefuelSurplus,
                    costs.FatalityRiskValue,
                    costs.NonFatalRiskValue,
                    costs.EconomicCosts,
                    costs.CongestionCosts,
                    costs.NoiseCosts,

                    // safety costs
                    costs.FatalityCostsOverall,
                    costs.FatalityCostsRebound,
                    costs.FatalityCostsDeltaCW,
                    costs.NonFatalInjuryCostsOverall,
                    costs.NonFatalInjuryCostsRebound,
                    costs.NonFatalInjuryCostsDeltaCW,
                    costs.PropertyDamageCostsOverall,
                    costs.PropertyDamageCostsRebound,
                    costs.PropertyDamageCostsDeltaCW
                };
            }
        }

        private void AppendEmissionCosts(SocietalCosts scenData, SocietalCosts baseData)
        {
            // NOTE:
            //  - for the baseline scenario all costs are written as absolutes
            //  - for alternative scenarios:
            //      - for incremental reports, the costs are written as incremental over the baseline
            //      - for absolute reports, the costs are written as absolute

            bool ignoreBaseData = this.Scenario.IsBaseline || !this.Flags.HasIncremental();
            double[] scenCosts = GetArrayOfCosts(scenData);
            double[] baseCosts = ignoreBaseData ? null : GetArrayOfCosts(baseData);

            this.AppendArrayOfValues(scenCosts, baseCosts);

            // local helper function
            double[] GetArrayOfCosts(SocietalCosts costs)
            {
                // emission damage costs should only be reported for the first 8 pollutants
                double[] costsArray = new double[5 + 3 * GhgEstimates.List.Length];
                double discRate = this._scenData.DiscountRate;
                int index = 0;

                // criteria pollutants are first 5 items
                foreach (Pollutant p in Pollutants.MonetizedList.Take(5)) {
                    costsArray[index++] = costs.GetEmissionsCosts(p, discRate, Estimates.Average);
                }

                // ghg pollutants are last 3 items
                foreach (Pollutant p in Pollutants.MonetizedList.Skip(5)) {
                    foreach (Estimates ghgEstimates in GhgEstimates.List) {
                        costsArray[index++] = costs.GetEmissionsCosts(p, discRate, ghgEstimates);
                    }
                }

                return costsArray;
            }
        }

        private void AppendSubtotals(SocietalCosts scenData, SocietalCosts baseData)
        {
            if (this.Scenario.IsBaseline) {
                this.AppendBuffer("0", 9);
                return;
            }

            double discRate = this._scenData.DiscountRate;

            double totalCosts = scenData.GetTotalSocialCosts(baseData);
            double[] totalBenefits = Array.ConvertAll(
                GhgEstimates.List, item => scenData.GetTotalSocialBenefits(discRate, item, baseData));

            this.AppendRounded(totalCosts);
            Array.ForEach(totalBenefits, benefits => this.AppendRounded(benefits));
            Array.ForEach(totalBenefits, benefits => this.AppendRounded(benefits - totalCosts));
        }

    }
}
