using System;
using System.Collections.Generic;

using Volpe.Cafe.EffectsData;
using Volpe.Cafe.EffectsData.Collections;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.Utils;

using FT = Volpe.Cafe.FuelType;
using RC = Volpe.Cafe.RegulatoryClass;

namespace Volpe.Cafe.Reporting.Generators
{
    /// <summary>
    /// Defines an enumerable object used for generating rows of data for an annual societal effects report or an
    /// annual societal effects summary report.
    /// </summary>
    public class AnnualSocietalEffectsReportDataGenerator : ReportDataGenerator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="AnnualSocietalEffectsReportDataGenerator"/> class, for the
        /// specified scenario and reporting data.
        /// </summary>
        /// <inheritdoc cref="ReportDataGenerator(Scenario, ReportingData, ModelingSettings)" path="/param"/>
        public AnnualSocietalEffectsReportDataGenerator(Scenario scen, ReportingData data, ModelingSettings settings)
            : base(scen, data, settings) { }

        private RCAltFuelValue _ageSum;
        private EnergyEffectsCollection _energyEffects;
        private EmissionEffectsCollection _emissionEffects;
        private SafetyEffectsCollection _safetyEffects;

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

            int startAge = Math.Max(0, this.Data.MinYear - year.Value);

            for (int age = startAge; age < Globals.CY; age++) {
                this.InitAnnualDataObjects(year.Value, age);
                var enumerator = this.GetYearEnumeratorHelper(year, age);

                while (enumerator.MoveNext()) {
                    yield return enumerator.Current;
                }
            }
        }

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

            this.InitSummaryDataObjects(year.Value);
            var enumerator = this.GetYearEnumeratorHelper(year, age: 0);

            while (enumerator.MoveNext()) {
                yield return enumerator.Current;
            }
        }

        private IEnumerator<object[]> GetYearEnumeratorHelper(int? year, int age = 0)
        {
            var dataFilters = this.Data.DataFilters;

            // start at "rc = 2" to skip DC and IC reg-classes
            for (int rc = 2; rc < RegClasses.ReportingList.Length; rc++) {
                if (!dataFilters.ReportRC[rc]) {
                    continue;
                }

                for (int ft = 0; ft < FuelTypes.ReportingList.Length; ft++) {
                    if (dataFilters.ReportFT[ft]) {
                        yield return this.GetValues(year, age, rc, ft);
                    }
                }
            }
        }

        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)
        {
            var effectsData = this.Data.ScenarioEffectsData[year].AnnualEffects[age];

            this._energyEffects = effectsData.EnergyEffects;
            this._emissionEffects = effectsData.EmissionEffects;
            this._safetyEffects = effectsData.SafetyEffects;
        }

        private void InitSummaryDataObjects(int calendarYear)
        {
            ResetData();

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

                this._energyEffects.Append(scenData.EnergyEffects);
                this._safetyEffects.Append(scenData.SafetyEffects);
                this._emissionEffects.Append(scenData.EmissionEffects);

                for (int rc = 0; rc < RegClasses.AltList.Length; rc++) {
                    foreach (FT fuelType in FuelTypes.List) {
                        double fleet = scenData.EnergyEffects[rc, fuelType.GetListIndex()].Fleet;
                        this._ageSum.Items[rc][fuelType] += fleet * age;
                    }
                }
            }

            void ResetData()
            {
                if (this._ageSum == null) {
                    this._ageSum = new RCAltFuelValue();
                    this._energyEffects = new EnergyEffectsCollection();
                    this._safetyEffects = new SafetyEffectsCollection();
                    this._emissionEffects = new EmissionEffectsCollection();
                }
                else {
                    this._ageSum.Clear();
                    this._energyEffects.Clear();
                    this._safetyEffects.Clear();
                    this._emissionEffects.Clear();

                }
            }
        }

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

            RC regClass = RegClasses.ReportingList[rc];
            FT fuelType = FuelTypes.ReportingList[ft];

            var energyEffects = this._energyEffects[regClass, fuelType];
            var safetyEffects = this._safetyEffects[regClass, fuelType];
            var emissionEffects = this._emissionEffects[regClass, fuelType];

            this.AppendHeader(year, age, rc, ft);
            this.AppendGeneralAndEnergyEffects(regClass, fuelType, energyEffects);
            this.AppendSafetyEffects(safetyEffects);
            this.AppendEmissionHealthImpacts(safetyEffects);
            this.AppendEmissionEffects(emissionEffects);

            return this.GetBuffer();
        }

        private void AppendHeader(int? year, int age, int rc, int ft)
        {
            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(FuelTypes.ReportingNames[ft]);
        }

        private void AppendGeneralAndEnergyEffects(RC regClass, FT fuelType, EnergyEffects energyEffects)
        {
            if (this.Flags.HasSummary()) {
                double fleet = energyEffects.Fleet;
                var avgFleet = (fleet == 0) ? 0 : this._ageSum[regClass, fuelType] / fleet;

                this.AppendRounded(avgFleet);
            }

            this.AppendRounded(energyEffects.Fleet);
            this.AppendRounded(energyEffects.VMT);
            this.AppendRounded(energyEffects.VMTNoRebound);
            this.AppendRounded(energyEffects.BTU / 1e12);
            this.AppendRounded(energyEffects.Gallons);

            if (fuelType == FT.All) {
                this.AppendEmpty(1);
            }
            else {
                this.AppendRounded(energyEffects.NativeUnits);
            }
        }

        private void AppendSafetyEffects(SafetyEffects safetyEffects)
        {
            this.AppendRounded(safetyEffects.FatalitiesOverall);
            this.AppendRounded(safetyEffects.FatalitiesRebound);
            this.AppendRounded(safetyEffects.FatalitiesDeltaCW);

            this.AppendRounded(safetyEffects.NonFatalInjuriesOverall);
            this.AppendRounded(safetyEffects.NonFatalInjuriesRebound);
            this.AppendRounded(safetyEffects.NonFatalInjuriesDeltaCW);

            this.AppendRounded(safetyEffects.PropertyDamageCrashesOverall);
            this.AppendRounded(safetyEffects.PropertyDamageCrashesRebound);
            this.AppendRounded(safetyEffects.PropertyDamageCrashesDeltaCW);
        }

        private void AppendEmissionHealthImpacts(SafetyEffects safetyEffects)
        {
            this.AppendArrayOfValues(safetyEffects.UpstreamHealthImpacts, null);
            this.AppendArrayOfValues(safetyEffects.TailpipeHealthImpacts, null);

            foreach (EmissionHealthImpact healthImpact in HealthImpacts.List) {
                this.AppendRounded(safetyEffects.GetTotalHealthImpacts(healthImpact));
            }
        }

        private void AppendEmissionEffects(EmissionEffects emissionEffects)
        {
            AppendEmissions(p => emissionEffects.Upstream[p]);
            AppendEmissions(p => emissionEffects.Tailpipe[p]);
            this.AppendRounded(emissionEffects.BrakeTireWearPM);
            AppendEmissions(p => emissionEffects.GetTotalEmissions(p));

            void AppendEmissions(Func<Pollutant, double> getEmissions)
            {
                foreach (Pollutant p in Pollutants.List) {
                    double emissions = getEmissions(p);
                    if (p == Pollutant.CO2) {
                        emissions /= 1e6; // convert to mmT
                    }
                    this.AppendRounded(emissions);
                }
            }
        }

    }
}
