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

namespace Volpe.Cafe.DfsSrModels
{
    /// <summary>
    /// Defines the method that a DFS/SR model may use to rescale sales projections of forecast vehicles.
    /// </summary>
    /// <param name="year">The model year being analyzed.</param>
    /// <param name="forecastVehs">A list of forecast vehicles for which to rescale sales projections.</param>
    /// <param name="prevSales">
    ///     The actual (or previously estimated) sales volumes for each fleet from a preceding model year.
    /// </param>
    /// <param name="newSales">The projected sales volumes for each fleet in the current model year.</param>
    [Serializable]
    public delegate void SalesRescalerDelegate(
        int year, IList<ForecastVehicle> forecastVehs, FleetTypeValue prevSales, FleetTypeValue newSales);

    /// <summary>
    /// Provides methods for scaling the sales projections of forecast vehicles using various criteria.
    /// </summary>
    [Serializable]
    public static class SalesRescaler
    {
        /// <summary>
        /// Adjusts sales projections of forecast vehicles to account for sales shifts attributed to any ZEV clones.
        /// </summary>
        /// <param name="year">The model year being analyzed.</param>
        /// <param name="forecastVehs">A list of forecast vehicles for which to adjust sales projections.</param>
        /// <remarks>
        /// The DFS model overwrites the sales for the ZEV candidate clones, negating any additional sales that may
        /// have been shifted over from the ZEV reference vehicles. Hence, this method is employed to adjust the final
        /// sales to maintain the initial ratios.
        /// </remarks>
        public static void AdjustForecastSalesForZev(int year, List<ForecastVehicle> forecastVehs)
        {
            var zevCandidates =
                forecastVehs.Where(fVeh => fVeh.Vehicle.IsZevCandidate && fVeh.Vehicle.ZevReferenceVehicle != null);

            foreach (var forecastVeh in forecastVehs) {
                var zevCandidate = forecastVeh.Vehicle;
                var zevReference = zevCandidate.ZevReferenceVehicle;
                if (!zevCandidate.IsZevCandidate || zevReference == null) {
                    continue;
                }

                // note: we use prior year revision info as the basis for our zev ratio b.c.
                // current year sales projections are also estimated w.r.t. previous year
                var zevCandidateRevision = zevCandidate.GetRevision(year - 1);
                var zevReferenceRevision = zevReference.GetRevision(year - 1);

                double combinedSales = zevCandidateRevision.Sales + zevReferenceRevision.Sales;
                if (combinedSales == 0) {
                    continue;
                }

                // ratio of initial sales that we had before DFS model forecasting
                double zevRatio = zevCandidateRevision.Sales / combinedSales;

                // adjust ZEV "candidate" and "reference" vehicle sales
                var zevCandidateForecastData = forecastVeh.ForecastData[year];
                var zevReferenceForecastData = forecastVehs.Find(x => x.Code == zevReference.Code).ForecastData[year];

                double totalPendingForecastSales = zevCandidateForecastData.Sales + zevReferenceForecastData.Sales;
                double zevCandidateFinalSales =
                    Math.Min(totalPendingForecastSales, Math.Ceiling(totalPendingForecastSales * zevRatio));

                zevCandidateForecastData.Sales = zevCandidateFinalSales;
                zevReferenceForecastData.Sales = totalPendingForecastSales - zevCandidateFinalSales;
            }
        }

        /// <summary>
        /// Rescales sales projections of forecast vehicles, using the vehicle's baseline <see cref="RegulatoryClass"/>
        /// assignment.
        /// </summary>
        /// <inheritdoc cref="SalesRescalerDelegate" path="/param"/>
        public static void RescaleSalesByBaselineRegClass(
            int year, IList<ForecastVehicle> forecastVehs, FleetTypeValue prevSales, FleetTypeValue newSales)
        {
            var salesMultiplier = GetSalesMultiplier(newSales, prevSales);

            foreach (var veh in forecastVehs) {
                var vehData = veh.ForecastData[year];
                var regClass = vehData.RevisionInfo.BaselineInfo.RegClass;
                vehData.Sales = Math.Round(veh.GetForecastSales(year - 1) * salesMultiplier[regClass]);
            }
        }

        /// <summary>
        /// Rescales sales projections of forecast vehicles, using the vehicle's <see cref="RegulatoryClass"/>
        /// assignment.
        /// </summary>
        /// <inheritdoc cref="SalesRescalerDelegate" path="/param"/>
        public static void RescaleSalesByRegClass(
            int year, IList<ForecastVehicle> forecastVehs, FleetTypeValue prevSales, FleetTypeValue newSales)
        {
            var salesMultiplier = GetSalesMultiplier(newSales, prevSales);

            foreach (var veh in forecastVehs) {
                var vehData = veh.ForecastData[year];
                var regClass = vehData.RegClass;
                vehData.Sales = Math.Round(veh.GetForecastSales(year - 1) * salesMultiplier[regClass]);
            }
        }

        /// <summary>
        /// Rescales sales projections of forecast vehicles, using the vehicle's <see cref="VehicleClass"/> assignment.
        /// </summary>
        /// <inheritdoc cref="SalesRescalerDelegate" path="/param"/>
        public static void RescaleSalesByVehClass(
            int year, IList<ForecastVehicle> forecastVehs, FleetTypeValue prevSales, FleetTypeValue newSales)
        {
            var salesMultiplier = GetSalesMultiplier(newSales, prevSales);

            foreach (var veh in forecastVehs) {
                var vehData = veh.ForecastData[year];
                var vehClass = DfsSrHelpers.GetBinningVehClass(vehData);
                vehData.Sales = Math.Round(veh.GetForecastSales(year - 1) * salesMultiplier[vehClass]);
            }
        }

        private static FleetTypeValue GetSalesMultiplier(FleetTypeValue numerator, FleetTypeValue denominator)
        {
            return new FleetTypeValue(
                (denominator.Car == 0) ? 1 : numerator.Car / denominator.Car,
                (denominator.Truck == 0) ? 1 : numerator.Truck / denominator.Truck,
                (denominator.Hdpuv == 0) ? 1 : numerator.Hdpuv / denominator.Hdpuv);
        }

    }
}
