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

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

        private SocietalEffectsReportData _scenData;
        private SocietalEffectsReportData _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)
        {
            this.InitDataObjects(year.Value);
            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, rc, ft);
                    }
                }
            }
        }

        private void InitDataObjects(int year)
        {
            this._scenData = new SocietalEffectsReportData(year, this.Data.ScenarioEffectsData);

            if (!this.Scenario.IsBaseline) {
                this._baseData = new SocietalEffectsReportData(year, this.Data.BaselineEffectsData);
            }
        }

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

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

            this._scenData.RefreshEffectsObjects(regClass, fuelType);
            this._baseData?.RefreshEffectsObjects(regClass, fuelType);

            this.AppendHeader(year, rc, ft);
            this.AppendFuelEconomyMetrics(year);
            this.AppendGeneralEffects(year);
            this.AppendUsageAndEnergyEffects(fuelType);
            this.AppendSafetyEffects();
            this.AppendEmissionHealthImpacts();
            this.AppendEmissionEffects();

            return this.GetBuffer();
        }

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

        private void AppendFuelEconomyMetrics(int? year)
        {
            if (year == null) {
                this.AppendEmpty(3);
                return;
            }

            this.AppendRounded(this._scenData.GetRatedFuelEconomy());
            this.AppendRounded(this._scenData.GetOnRoadFuelEconomy());
            this.AppendRounded(this._scenData.GetFuelShare());
        }

        private void AppendGeneralEffects(int? year)
        {
            var scenData = this._scenData.GeneralEffects;
            bool isHistoricYear = (year != null && year.Value < this.Data.MinYear);

            if (year == null || isHistoricYear) {
                this.AppendEmpty(3);
            }
            else {
                this.AppendRounded(scenData.CurbWeight);
                this.AppendRounded(scenData.Footprint);
                this.AppendRounded(scenData.WorkFactor);
            }

            if (isHistoricYear) {
                this.AppendEmpty(1);
            }
            else {
                this.AppendBuffer(scenData.Sales);
            }
        }

        private void AppendUsageAndEnergyEffects(FuelType fuelType)
        {
            // NOTE:
            //  - for the baseline scenario all effects are written as absolutes
            //  - for alternative scenarios:
            //      - for "incremental" reporting, the effects are written as incremental over the baseline
            //      - for "absolute" reporting, the effects are written as absolute

            bool ignoreBaseData = this.Scenario.IsBaseline || !this.Flags.HasIncremental();
            double[] scenEffects = this._scenData.GetArrayOfUsageAndEnergyEffects();
            double[] baseEffects = ignoreBaseData ? null : this._baseData.GetArrayOfUsageAndEnergyEffects();

            this.AppendArrayOfValues(scenEffects, baseEffects);

            // append native units separately
            if (fuelType == FuelType.All) {
                this.AppendEmpty(1);
            }
            else {
                double scenValue = this._scenData.EnergyEffects.NativeUnits;
                double baseValue = ignoreBaseData ? 0 : this._baseData.EnergyEffects.NativeUnits;

                this.AppendRounded(scenValue - baseValue);
            }
        }

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

            bool ignoreBaseData = this.Scenario.IsBaseline || !this.Flags.HasIncremental();
            double[] scenEffects = this._scenData.GetArrayOfSafetyEffects();
            double[] baseEffects = ignoreBaseData ? null : this._baseData.GetArrayOfSafetyEffects();

            this.AppendArrayOfValues(scenEffects, baseEffects);
        }

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

            bool ignoreBaseData = this.Scenario.IsBaseline || !this.Flags.HasIncremental();
            double[] scenEffects = this._scenData.GetArrayOfHealthImpacts();
            double[] baseEffects = ignoreBaseData ? null : this._baseData.GetArrayOfHealthImpacts();

            this.AppendArrayOfValues(scenEffects, baseEffects);
        }

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

            bool ignoreBaseData = this.Scenario.IsBaseline || !this.Flags.HasIncremental();
            double[] scenEffects = this._scenData.GetArrayOfEmissionEffects();
            double[] baseEffects = ignoreBaseData ? null : this._baseData.GetArrayOfEmissionEffects();

            this.AppendArrayOfValues(scenEffects, baseEffects);
        }

        [Serializable]
        private sealed class SocietalEffectsReportData
        {
            public SocietalEffectsReportData(int year, IAnnualValue<ModelingEffects> effectsData)
            {
                this.EffectsData = effectsData[year].AggregateEffects;
            }

            private RegulatoryClass _regClass;
            private FuelType _fuelType;

            public ModelingEffectsItem EffectsData { get; }
            public GeneralEffects GeneralEffects { get; private set; }
            public EnergyEffects EnergyEffects { get; private set; }
            public EmissionEffects EmissionEffects { get; private set; }
            public SafetyEffects SafetyEffects { get; private set; }

            public double GetRatedFuelEconomy()
            {
                return this.EnergyEffects.RatedFuelEconomy;
            }

            public double GetOnRoadFuelEconomy()
            {
                return this.EnergyEffects.OnRoadFuelEconomy;
            }

            public double GetFuelShare()
            {
                return this.EffectsData.EnergyEffects.GetFuelShare(this._regClass, this._fuelType);
            }

            public double[] GetArrayOfUsageAndEnergyEffects()
            {
                return new double[] {
                    this.EnergyEffects.VMT,
                    this.EnergyEffects.VMTNoRebound,
                    this.EnergyEffects.BTU / 1e12, // convert to quads
                    this.EnergyEffects.Gallons
                };
            }

            public double[] GetArrayOfSafetyEffects()
            {
                return new double[] {
                    this.SafetyEffects.FatalitiesOverall,
                    this.SafetyEffects.FatalitiesRebound,
                    this.SafetyEffects.FatalitiesDeltaCW,
                    this.SafetyEffects.NonFatalInjuriesOverall,
                    this.SafetyEffects.NonFatalInjuriesRebound,
                    this.SafetyEffects.NonFatalInjuriesDeltaCW,
                    this.SafetyEffects.PropertyDamageCrashesOverall,
                    this.SafetyEffects.PropertyDamageCrashesRebound,
                    this.SafetyEffects.PropertyDamageCrashesDeltaCW
                };
            }

            public double[] GetArrayOfHealthImpacts()
            {
                int count = HealthImpacts.List.Length;
                double[] values = new double[count * 3];

                Array.Copy(this.SafetyEffects.UpstreamHealthImpacts, 0, values, 0, count);
                Array.Copy(this.SafetyEffects.TailpipeHealthImpacts, 0, values, count, count);

                for (int i = 0, j = count * 2; i < count; i++) {
                    values[j++] = values[i] + values[i + count];
                }

                return values;
            }

            public double[] GetArrayOfEmissionEffects()
            {
                int count = Pollutants.List.Length;
                double[] values = new double[count * 3 + 1];

                Array.Copy(this.EmissionEffects.Upstream.Items, 0, values, 0, count);
                Array.Copy(this.EmissionEffects.Tailpipe.Items, 0, values, count, count);

                // emissions are in metric tons -- for CO2, convert to million metric tons (mmt)
                int co2Index = (int)Pollutant.CO2;
                values[co2Index] /= 1e6;
                values[co2Index + count] /= 1e6;

                int j = count * 2;
                values[j++] = this.EmissionEffects.BrakeTireWearPM;

                for (int i = 0; i < count; i++) {
                    values[j++] = values[i] + values[i + count];
                }

                // for PM2.5, also include brake & tire wear in the total
                values[(int)Pollutant.PM + count * 2 + 1] += this.EmissionEffects.BrakeTireWearPM;

                return values;
            }

            public void RefreshEffectsObjects(RegulatoryClass regClass, FuelType fuelType)
            {
                this._regClass = regClass;
                this._fuelType = fuelType;

                this.GeneralEffects = this.EffectsData.GeneralEffects[regClass, fuelType];
                this.EnergyEffects = this.EffectsData.EnergyEffects[regClass, fuelType];
                this.EmissionEffects = this.EffectsData.EmissionEffects[regClass, fuelType];
                this.SafetyEffects = this.EffectsData.SafetyEffects[regClass, fuelType];
            }

        }

    }
}
