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 an annual societal costs report or an annual
    /// societal costs summary report.
    /// </summary>
    public class AnnualSocietalCostsReportDataGenerator : ReportDataGenerator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="AnnualSocietalCostsReportDataGenerator"/> class, for the
        /// specified scenario and reporting data.
        /// </summary>
        /// <inheritdoc cref="ReportDataGenerator(Scenario, ReportingData, ModelingSettings)" path="/param"/>
        public AnnualSocietalCostsReportDataGenerator(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 = this.Flags.HasSummary()
                ? this.Data.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)
        {
            return this.Flags.HasSummary() ? this.SummaryYearEnumerator(year) : this.AnnualYearEnumerator(year);
        }

        private IEnumerator<object[]> AnnualYearEnumerator(int? year)
        {
            if (!this.HasData(year)) {
                yield break;
            }

            var dataFilters = this.Data.DataFilters;
            int drCount = this.Settings.Parameters.EconomicValues.DiscountRate.Length;
            int startAge = Math.Max(0, this.Data.MinYear - year.Value);

            for (int dr = -1; dr < drCount; dr++) {
                for (int age = startAge; age < Globals.CY; age++) {
                    this.InitAnnualDataObjects(year.Value, age, dr);

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

        private IEnumerator<object[]> SummaryYearEnumerator(int? year)
        {
            if (!this.HasData(year)) {
                yield break;
            }

            var dataFilters = this.Data.DataFilters;
            int drCount = this.Settings.Parameters.EconomicValues.DiscountRate.Length;

            for (int dr = -1; dr < drCount; dr++) {
                this.InitSummaryDataObjects(year.Value, dr);

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

        private bool HasData(int? year)
        {
            // for simplicity, assume that if aggregate effects exist, annual effects also exist at relevant ages
            var scenData = this.Data.ScenarioEffectsData?[year.Value]?.AggregateEffects;
            return scenData != null
                && scenData.SocialCosts != null
                && scenData.DiscSocialCosts != null;
        }

        private void InitAnnualDataObjects(int year, int age, int dr)
        {
            var scenData = this.Data.ScenarioEffectsData[year].AnnualEffects[age];
            this._scenData = (dr == -1) ? scenData.SocialCosts : scenData.DiscSocialCosts[dr];

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

        private void InitSummaryDataObjects(int calendarYear, int dr)
        {
            bool isUndiscounted = (dr == -1);
            double discRate = isUndiscounted ? 0 : this.Settings.Parameters.EconomicValues.DiscountRate[dr];

            this._scenData = new SocietalCostsCollection(discRate);
            if (!this.Scenario.IsBaseline) {
                this._baseData = new SocietalCostsCollection(discRate);
            }

            for (int age = Globals.CY - 1; age >= 0; age--) {
                int modelYear = calendarYear - age;

                // check to make sure we don't go below the minimum year
                if (modelYear < this.Data.MinEffectsYear) {
                    break;
                }

                var scenData = this.Data.ScenarioEffectsData[modelYear]?.AnnualEffects[age];
                if (scenData == null) {
                    continue;
                }

                var socCosts = isUndiscounted ? scenData.SocialCosts : scenData.DiscSocialCosts[dr];
                this._scenData.Append(socCosts);

                if (!this.Scenario.IsBaseline) {
                    var baseData = this.Data.BaselineEffectsData[modelYear].AnnualEffects[age];
                    socCosts = isUndiscounted ? baseData.SocialCosts : baseData.DiscSocialCosts[dr];
                    this._baseData.Append(socCosts);
                }
            }
        }

        private object[] GetValues(int? year, int age, 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, age, rc);
            this.AppendEconAndSafetyCosts(scenData);
            this.AppendEmimssionCosts(scenData);
            this.AppendSubtotals(scenData, baseData);

            return this.GetBuffer();
        }

        private void AppendHeader(int? year, int age, int rc)
        {
            this.AppendBuffer(this.Scenario.Index);
            this.AppendBuffer(StringHelper.GetTitleCase(this.Scenario.Description));
            this.AppendBuffer(year.ToString());

            if (!this.Flags.HasSummary()) {
                this.AppendBuffer(age.ToString());
                this.AppendBuffer((year + age).ToString());
            }

            this.AppendBuffer(RegClasses.ReportingNames[rc]);
            this.AppendBuffer(this._scenData.DiscountRate);
        }

        private void AppendEconAndSafetyCosts(SocietalCosts scenData)
        {
            // tech, usage, and energy costs
            this.AppendRounded(scenData.NewSalesSurplus);
            this.AppendRounded(scenData.TechCosts);
            this.AppendRounded(scenData.MaintRepairCosts);
            this.AppendRounded(scenData.OpportunityCost);
            this.AppendRounded(scenData.CommercialOppCost);
            this.AppendRounded(scenData.FuelTaxCosts);
            this.AppendRounded(scenData.RetailFuelCosts);
            this.AppendRounded(scenData.DriveSurplus);
            this.AppendRounded(scenData.RefuelSurplus);
            this.AppendRounded(scenData.FatalityRiskValue);
            this.AppendRounded(scenData.NonFatalRiskValue);
            this.AppendRounded(scenData.EconomicCosts);
            this.AppendRounded(scenData.CongestionCosts);
            this.AppendRounded(scenData.NoiseCosts);

            // safety costs
            this.AppendRounded(scenData.FatalityCostsOverall);
            this.AppendRounded(scenData.FatalityCostsRebound);
            this.AppendRounded(scenData.FatalityCostsDeltaCW);
            this.AppendRounded(scenData.NonFatalInjuryCostsOverall);
            this.AppendRounded(scenData.NonFatalInjuryCostsRebound);
            this.AppendRounded(scenData.NonFatalInjuryCostsDeltaCW);
            this.AppendRounded(scenData.PropertyDamageCostsOverall);
            this.AppendRounded(scenData.PropertyDamageCostsRebound);
            this.AppendRounded(scenData.PropertyDamageCostsDeltaCW);
        }

        private void AppendEmimssionCosts(SocietalCosts scenData)
        {
            // emission damage costs should only be reported for the first 8 pollutants
            double discRate = this._scenData.DiscountRate;

            // criteria pollutants are first 5 items
            foreach (Pollutant p in Pollutants.MonetizedList.Take(5)) {
                this.AppendRounded(scenData.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) {
                    this.AppendRounded(scenData.GetEmissionsCosts(p, discRate, ghgEstimates));
                }
            }
        }

        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));
        }

    }
}
