using System.Collections.Generic;

using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.TechKeys;

namespace Volpe.Cafe.EffectsModels
{
    /// <summary>
    /// Provides methods for aggregating vehicle-level data from one or more vehicle models into a list of one or more
    /// aggregate vehicle objects.
    /// </summary>
    public class VehicleAggregator
    {
        #region <<< Helper Classes >>>

        /// <summary>
        /// Defines a class used by <see cref="VehicleAggregator"/> to store aggregate vehicle data during aggregation
        /// and create new aggregate vehicles.
        /// </summary>
        private class AggregatorInfo
        {
            #region <<< Constructors >>>

            /// <summary>
            /// Initializes a new instance of the <see cref="AggregatorInfo"/> class, for vehicles of the specified 
            /// regulatory class and vehicle style for the given <see cref="VehicleAggregator"/>.
            /// </summary>
            /// <param name="regClass">The regulatory class used to initialize this instance.</param>
            /// <param name="style">The vehicle style used to initialize this instance.</param>
            /// <param name="aggregator">The vehicle aggregator using this instance.</param>
            public AggregatorInfo(RegulatoryClass regClass, VehicleStyle style, VehicleAggregator aggregator)
            {
                this.Sales = 0;
                this.Msrp = 0;
                this.FuelShare = FuelValue.Zero;
                this.FuelEconomy = FuelValue.Zero;
                this.RatedFE = FuelValue.Zero;
                this.OnRoadFE = FuelValue.Zero;
                this.InitFuelShare = FuelValue.Zero;
                this.InitOnRoadFE = FuelValue.Zero;

                this.CurbWeight = 0;
                this.InitCurbWeight = 0;
                this.Gvwr = 0;
                this.Gcwr = 0;

                this.RegClass = regClass;
                this.Style = style;
                this.MfrName = string.Empty;
                this.Aggregator = aggregator;
            }

            #endregion

            #region <<< Properties >>>

            private RegulatoryClass RegClass { get; set; }
            private VehicleStyle Style { get; set; }
            private SafetyClass SafetyClass => SafetyClass.None;
            private TechnologyClass TechnologyClass => TechnologyClass.None;
            private EngineTechnologyClass EngineTechnologyClass => EngineTechnologyClass.None;

            private string MfrName { get; set; }

            private double Sales { get; set; }
            private double Msrp { get; set; }
            private double Footprint { get; set; }
            private double CurbWeightMR0 { get; set; }
            private double CurbWeight { get; set; }
            private double Gvwr { get; set; }
            private double Gcwr { get; set; }
            private double FuelCapacity { get; set; }

            private FuelValue FuelShare { get; set; }

            private FuelValue FuelEconomy { get; set; }
            private FuelValue FuelEconomyDenominator { get; set; }

            private FuelValue RatedFE { get; set; }
            private FuelValue RatedFEDenominator { get; set; }

            private FuelValue OnRoadFE { get; set; }
            private FuelValue OnRoadFEDenominator { get; set; }

            private FuelValue InitFuelShare { get; set; }

            private FuelValue InitFuelEconomy { get; set; }
            private FuelValue InitFEDenominator { get; set; }

            private FuelValue InitRatedFE { get; set; }
            private FuelValue InitRatedFEDenominator { get; set; }

            private FuelValue InitOnRoadFE { get; set; }
            private FuelValue InitOnRoadFEDenominator { get; set; }

            private double InitCurbWeight { get; set; }

            private HEVType HevType => HEVType.All;
            private TechKey128 TechKey => TechKey128.Empty;

            private VehicleAggregator Aggregator { get; }

            #endregion

            #region <<< Methods >>>

            /// <summary>
            /// Performs calculations and appends the sales-weighted vehicle data to the calling instance of
            /// <see cref="AverageData"/>.
            /// </summary>
            /// <param name="veh">The vehicle from which to append data.</param>
            /// <param name="year">The year of the vehicle revisions to use when appending data.</param>
            public void AppendInfo(IVehicle veh, int year)
            {
                ModelingSettings settings = this.Aggregator.Settings;

                if (this.MfrName == string.Empty) {
                    this.MfrName = veh.ManufacturerName;
                }
                if (this.MfrName != veh.ManufacturerName) {
                    this.MfrName = "Various";
                }

                var vehRevision = veh.GetRevision(year);
                var vehBaseInfo = veh.BaselineInfo;
                var scen = this.Aggregator.Scenario;

                this.Sales += vehRevision.Sales;
                this.Msrp += vehRevision.Msrp * vehRevision.Sales;
                this.Footprint += vehRevision.Footprint * vehRevision.Sales;
                this.CurbWeightMR0 += vehRevision.CurbWeightMR0 * vehRevision.Sales;
                this.CurbWeight += vehRevision.CurbWeight * vehRevision.Sales;
                this.Gvwr += vehRevision.GVWR * vehRevision.Sales;
                this.Gcwr += vehRevision.GCWR * vehRevision.Sales;
                this.FuelCapacity += vehRevision.FuelCapacity * vehRevision.Sales;
                this.InitCurbWeight += vehBaseInfo.CurbWeight * vehRevision.Sales;

                FuelValue weightedFuelShare = vehRevision.FuelShare * vehRevision.Sales;
                this.FuelShare += weightedFuelShare;

                this.FuelEconomyDenominator += FuelValue.SafeDivide(weightedFuelShare, vehRevision.FuelEconomy);

                FuelValue ratedFE = Effects.CalcRatedVehicleFE(scen, year, vehRevision);
                this.RatedFEDenominator += FuelValue.SafeDivide(weightedFuelShare, ratedFE);

                FuelValue onRoadFE = Effects.ConvertToOnRoadFE(ratedFE, vehRevision.Style, settings);
                this.OnRoadFEDenominator += FuelValue.SafeDivide(weightedFuelShare, onRoadFE);

                FuelValue weightedInitFS = vehBaseInfo.FuelShare * vehRevision.Sales;
                this.InitFuelShare += weightedInitFS;

                this.InitFEDenominator += FuelValue.SafeDivide(weightedInitFS, vehBaseInfo.FuelEconomy);

                FuelValue initRatedFE = Effects.CalcRatedVehicleFE(scen, year, vehBaseInfo);
                this.InitRatedFEDenominator += FuelValue.SafeDivide(weightedInitFS, initRatedFE);

                FuelValue initOnRoadFE = Effects.ConvertToOnRoadFE(initRatedFE, vehRevision.Style, settings);
                this.InitOnRoadFEDenominator += FuelValue.SafeDivide(weightedInitFS, initOnRoadFE);
            }

            /// <summary>
            /// Performs division operations on aggregate vehicle data so that each property reflects a sales-weighted
            /// average of appended vehicles prior to generating an <see cref="AggregateEffectsVehicle"/>.
            /// Only to be called by <see cref="GenerateAggregateVehicle"/>.
            /// </summary>
            private void AverageData()
            {
                this.Msrp /= this.Sales;
                this.Footprint /= this.Sales;
                this.CurbWeightMR0 /= this.Sales;
                this.CurbWeight /= this.Sales;
                this.Gvwr /= this.Sales;
                this.Gcwr /= this.Sales;
                this.FuelCapacity /= this.Sales;
                this.InitCurbWeight /= this.Sales;

                this.FuelEconomy = FuelValue.SafeDivide(this.FuelShare, this.FuelEconomyDenominator);
                this.RatedFE = FuelValue.SafeDivide(this.FuelShare, this.RatedFEDenominator);
                this.OnRoadFE = FuelValue.SafeDivide(this.FuelShare, this.OnRoadFEDenominator);

                // This must be calculated after FE values so can be used as the numerator in those calculations.
                this.FuelShare /= this.Sales;

                this.InitFuelEconomy = FuelValue.SafeDivide(this.InitFuelShare, this.InitFEDenominator);
                this.InitRatedFE = FuelValue.SafeDivide(this.InitFuelShare, this.InitRatedFEDenominator);
                this.InitOnRoadFE = FuelValue.SafeDivide(this.InitFuelShare, this.InitOnRoadFEDenominator);

                // This must be calculated after InitFE values so can be used as the numerator in those calculations.
                this.InitFuelShare /= this.Sales;
            }

            /// <summary>
            /// Averages data and generates an <see cref="AggregateEffectsVehicle"/> with the data.
            /// </summary>
            public AggregateEffectsVehicle GenerateAggregateVehicle(int year)
            {
                this.AverageData();

                var aggregateVeh = new AggregateEffectsVehicle(this.RegClass, this.Style, year) {
                    ManufacturerName = this.MfrName,
                    FuelEconomy = this.FuelEconomy,
                    FuelShare = this.FuelShare,
                    Sales = this.Sales,
                    Msrp = this.Msrp,
                    SafetyClass = this.SafetyClass,
                    TechnologyClass = this.TechnologyClass,
                    EngineTechnologyClass = this.EngineTechnologyClass,
                    Footprint = this.Footprint,
                    CurbWeightMR0 = this.CurbWeightMR0,
                    CurbWeight = this.CurbWeight,
                    GVWR = this.Gvwr,
                    GCWR = this.Gcwr,
                    FuelCapacity = this.FuelCapacity,
                    HevType = this.HevType,
                    TechKey = this.TechKey,
                    RatedFE = this.RatedFE,
                    OnRoadFE = this.OnRoadFE,
                    InitFuelShare = this.InitFuelShare,
                    InitFuelEconomy = this.InitFuelEconomy,
                    InitRatedFE = this.InitRatedFE,
                    InitOnRoadFE = this.InitOnRoadFE,
                    InitCurbWeight = this.InitCurbWeight
                };

                return aggregateVeh;
            }

            public bool HasSales()
            {
                return this.Sales > 0;
            }

            #endregion
        }

        #endregion

        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="VehicleAggregator"/> class, using the specified list of
        /// vehicles.
        /// </summary>
        /// <param name="vehs">The list of vehicles that is the source for aggregation.</param>
        /// <param name="scen">The compliance scenario being analyzed.</param>
        /// <param name="settings">The modeling settings used for analysis.</param>
        public VehicleAggregator(IReadOnlyList<IVehicle> vehs, Scenario scen, ModelingSettings settings)
        {
            this.Vehicles = vehs;
            this.Scenario = scen;
            this.Settings = settings;
        }

        #endregion

        #region <<< Properties >>>

        private IReadOnlyList<IVehicle> Vehicles { get; }
        private Scenario Scenario { get; }
        private ModelingSettings Settings { get; }

        #endregion

        #region <<< Methods >>>

        /// <summary>
        /// Aggregates and returns the vehicle-level data for vehicle revisions of the specified year into a list of
        /// <see cref="AggregateEffectsVehicle"/>.
        /// </summary>
        public IReadOnlyList<AggregateEffectsVehicle> Aggregate(int year)
        {
            var aggregationBuckets = this.InitializeDictionary(RegClasses.AltAcronyms, VehicleStyles.AltNames);
            foreach (var veh in this.Vehicles) {
                this.AppendVehicle(veh, aggregationBuckets, year);
            }

            var agVehsList = new List<AggregateEffectsVehicle>();
            foreach (AggregatorInfo info in aggregationBuckets.Values) {
                if (info.HasSales()) {
                    AggregateEffectsVehicle veh = info.GenerateAggregateVehicle(year);
                    agVehsList.Add(veh);
                }
            }

            return agVehsList.AsReadOnly();
        }

        /// <summary>
        /// Initializes aggregation buckets as a dictionary containing a new <see cref="AggregatorInfo"/> for each
        /// permutation of <see cref="RegulatoryClass"/> and <see cref="VehicleStyle"/>.
        /// </summary>
        /// <param name="regClasses">This list of reg-classes from which to make aggregation buckets.</param>
        /// <param name="styles">The list of vehicle styles from which to make aggregation buckets.</param>
        /// <returns>
        /// A dictionary containing each permutation of regulatory classes and vehicle styles with  a string key
        /// consisting of a concatenation of the input arrays.
        /// </returns>
        private Dictionary<string, AggregatorInfo> InitializeDictionary(string[] regClasses, string[] styles)
        {
            Dictionary<string, AggregatorInfo> aggregationBuckets = new Dictionary<string, AggregatorInfo>();
            for (int regClass = 0; regClass < regClasses.Length; regClass++) {
                for (int style = 0; style < styles.Length; style++) {
                    string key = $"{regClasses[regClass]}_{styles[style]}";
                    aggregationBuckets.Add(key,
                        new AggregatorInfo(RegClasses.AltList[regClass], VehicleStyles.AltList[style], this));
                }
            }
            return aggregationBuckets;
        }

        private string VehicleToDictionaryKey(IVehicleRevision vehRevision)
        {
            string key = $"{RegClasses.AltAcronyms[vehRevision.RegClass.GetAltListIndex()]}_" +
                         $"{VehicleStyles.AltNames[vehRevision.Style.GetAltListIndex()]}";
            return key;
        }

        private void AppendVehicle(IVehicle veh, Dictionary<string, AggregatorInfo> aggregationBuckets, int year)
        {
            string key = this.VehicleToDictionaryKey(veh.GetRevision(year));
            aggregationBuckets[key].AppendInfo(veh, year);
        }

        #endregion
    }
}
