#region <<< Using Directives >>>
using System;
using System.Collections.Generic;
using System.Linq;

using Volpe.Cafe.DfsSrModels.Aggregators;
using Volpe.Cafe.DfsSrModels.DfsModels;
using Volpe.Cafe.DfsSrModels.SalesEstimators;
using Volpe.Cafe.Generic;
using Volpe.Cafe.Logging;
using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.ModelSettings;
#endregion

namespace Volpe.Cafe.DfsSrModels
{
    /// <summary>
    /// Provides the methodology to support the Dynamic Fleet Share and Sales Response (DFS/SR) model, which forecasts
    /// new vehicle sales in each future model year.
    /// </summary>
    [Serializable]
    public class DfsSrModel
    {
        /// <summary>
        /// Initializes a new instances of the <see cref="DfsSrModel"/> class, using the specified industry
        /// data, modeling settings, and log writer.
        /// </summary>
        /// <param name="data">The product plan industry data used for analysis.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        /// <param name="logWriter">The object used for writing log files generated during compliance modeling.</param>
        /// <exception cref="ArgumentNullException">data, settings, or logWriter is null.</exception>
        public DfsSrModel(IIndustry data, ModelingSettings settings, ILogger logWriter)
        {
            ThrowHelper.ThrowIfInvalidArgs(data, settings, logWriter);

            this.Data = data;
            this.Settings = settings;
            this.LogWriter = logWriter;

            this.SalesEstimator = SalesEstimatorFactory.CreateSalesEstimator(data, settings, logWriter);
            this.DfsModel = DfsModelFactory.CreateDfsModel(data, settings, logWriter);

            this.MinYear = settings.RuntimeSettings.AnalysisStartYear;
            this.MaxYear = settings.RuntimeSettings.AnalysisEndYear;

            // need to keep track of one historic year (at a minimum) b.c. our models are
            // lagged and we rescale current year's sales based on previous year's sales
            this.NumberOfLaggedYears = Math.Max(1, this.DfsModel.NumberOfLaggedYears);
            this.LaggedMinYear = this.MinYear - this.NumberOfLaggedYears;

            this.AggregationMode = settings.RuntimeSettings.DfsSrAggregationMode;
            if (this.AggregationMode == AggregationMode.Auto) {
                this.AggregationMode = this.DfsModel.PreferredAggregationMode;
            }

            switch (this.AggregationMode) {
                case AggregationMode.ByVehicleClass:
                    this.FleetAggregator = new ForecastFleetAggregatorVehClass(settings);
                    this.SalesRescalerMethod = SalesRescaler.RescaleSalesByVehClass;
                    break;

                case AggregationMode.ByRegulatoryClass:
                    this.FleetAggregator = new ForecastFleetAggregatorRegClass(settings, false);
                    this.SalesRescalerMethod = SalesRescaler.RescaleSalesByRegClass;
                    break;

                case AggregationMode.ByBaselineRegClass:
                default:
                    this.FleetAggregator = new ForecastFleetAggregatorRegClass(settings, true);
                    this.SalesRescalerMethod = SalesRescaler.RescaleSalesByBaselineRegClass;
                    break;
            }

            if (logWriter.LogLevel == LogLevel.Debug) {
                this.IterationLog = new DfsSrIterationWriter(settings, logWriter);
            }

            this.BaseFleet = new AnnualValue<ForecastFleet>(this.LaggedMinYear, this.MaxYear, outOfBoundsMode: -1);
        }

        /// <summary>
        /// Gets the product plan industry data used for analysis.
        /// </summary>
        public IIndustry Data { get; }

        /// <summary>
        /// Gets the modeling settings used for analysis.
        /// </summary>
        public ModelingSettings Settings { get; }

        /// <summary>
        /// Gets the object used for writing log files generated during compliance modeling.
        /// </summary>
        public ILogger LogWriter { get; }

        private ISalesEstimator SalesEstimator { get; }
        private IDfsModel DfsModel { get; }
        private IForecastFleetAggregator FleetAggregator { get; }
        private SalesRescalerDelegate SalesRescalerMethod { get; }

        private AggregationMode AggregationMode { get; }
        private int NumberOfLaggedYears { get; }
        private int MinYear { get; }
        private int MaxYear { get; }
        private int LaggedMinYear { get; }

        private DfsSrIterationWriter IterationLog { get; }
        private AnnualValue<ForecastFleet> BaseFleet { get; }

        /// <summary>
        /// Estimates the new vehicle sales volumes for the given scenario, iteration, and all model years, for the
        /// specified list of vehicles.
        /// </summary>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="iteration">The iteration trial being analyzed.</param>
        /// <param name="vehs">The list of vehicles for which to forecast new sales for all model years.</param>
        /// <returns>The list of forecast vehicles, containing the estimated sales for each model year.</returns>
        public List<ForecastVehicle> ForecastVehicleSales(Scenario scen, int iteration, IReadOnlyList<IVehicle> vehs)
        {
            var forecastVehs = vehs.Select(veh => new ForecastVehicle(veh)).ToList();
            var forecastFleet = new AnnualValue<ForecastFleet>(
                this.LaggedMinYear, this.MaxYear, outOfBoundsMode: -1);

            for (int year = this.LaggedMinYear; year <= this.MaxYear; year++) {
                forecastFleet[year] = this.FleetAggregator.AggregateFleetData(scen, year, forecastVehs);
            }

            for (int year = this.MinYear + 1; year <= this.MaxYear; year++) {
                this.ForecastVehicleSales(scen, iteration, year, forecastVehs, forecastFleet);
                this.IterationLog?.LogIterationResults(scen, iteration, year, forecastVehs);

                // update to reflect new sales forecast
                forecastFleet[year] = this.FleetAggregator.AggregateFleetData(scen, year, forecastVehs);
            }

            return forecastVehs;
        }

        private void ForecastVehicleSales(Scenario scen, int iteration, int year,
            List<ForecastVehicle> forecastVehs, AnnualValue<ForecastFleet> forecastFleet)
        {
            var scenFleetLag1 = forecastFleet[year - 1];
            var scenFleet = forecastFleet[year];
            var baseFleet = scen.IsBaseline ? scenFleet : this.BaseFleet[year];

            var newFleetShare = this.DfsModel.CalcNewFleetShares(scen, iteration, year, this.BaseFleet, forecastFleet);
            double newLDSales = this.SalesEstimator.CalcNewLightDutySales(scen, iteration, year, baseFleet, scenFleet);
            double newCarSales = newLDSales * newFleetShare.Car;
            double newTrkSales = newLDSales * newFleetShare.Truck;
            double new2b3Sales = this.SalesEstimator.CalcNewHdpuvSales(scen, iteration, year, baseFleet, scenFleet);

            var newSales = new FleetTypeValue(newCarSales, newTrkSales, new2b3Sales);
            var prevSales = new FleetTypeValue(scenFleetLag1.CarForecastData.Sales,
                                               scenFleetLag1.TruckForecastData.Sales,
                                               scenFleetLag1.HdpuvForecastData.Sales);

            this.SalesRescalerMethod(year, forecastVehs, prevSales, newSales);
            SalesRescaler.AdjustForecastSalesForZev(year, forecastVehs);

            // Baseline fleet should be saved for the second to last iteration, because that
            // represents the **final** baseline data used for compliance. However, if the total
            // number of iterations is one (i.e., no feedback), save those results also.
            // When evaluating alternative scenarios, the saved baseline fleet may be used as
            // reference for DFS models that propagate or adjust based on the baseline fleet.
            int iterationCap = Math.Max(1, this.Settings.RuntimeSettings.DfsSrIterations - 1);
            if (scen.IsBaseline && iteration == iterationCap) {
                this.BaseFleet[year] = scenFleet;
            }
        }

    }
}
