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

using Volpe.Cafe.MarketData.Design;
using Volpe.Cafe.Model;
using Volpe.Cafe.ModelSettings;
using Volpe.Cafe.Utils;

using TechKey = Volpe.Cafe.TechKeys.TechKey128;
using TI = Volpe.Cafe.TechnologyIndexes;

namespace Volpe.Cafe.Reporting.Generators
{
    /// <summary>
    /// Defines an enumerable object used for generating rows of data for different types of components reports.
    /// </summary>
    /// <typeparam name="T">The type of component to generate a report for.</typeparam>
    [Serializable]
    public abstract class ComponentReportDataGeneratorBase<T> : ReportDataGenerator where T : IOEMComponent
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ComponentReportDataGeneratorBase{T}"/> class, for the
        /// specified scenario and reporting data.
        /// </summary>
        /// <inheritdoc cref="ReportDataGenerator(Scenario, ReportingData, ModelingSettings)" path="/param"/>
        public ComponentReportDataGeneratorBase(Scenario scen, ReportingData data, ModelingSettings settings)
            : base(scen, data, settings) { }

        // count of veh-info cols: "Sales Initial,Sales,...,TechKey Initial,TechKey"
        private const int NumAggregateVehInfoValues = 20;

        /// <inheritdoc/>
        protected override bool WriteYearTotals => false;

        /// <inheritdoc/>
        protected override IEnumerator<object[]> GetYearEnumerator(int? year)
        {
            foreach (var mfr in this.Data.ScenarioData.Manufacturers) {
                var components = this.GetComponentList(mfr);
                var vehs = mfr.Vehicles;

                foreach (var component in components) {
                    int yearValue = year.Value;
                    var allReportData = this.GetReportData(yearValue, component, vehs);

                    foreach (var reportData in allReportData) {
                        yield return this.GetValues(reportData);
                    }
                }
            }
        }

        /// <summary>
        /// Returns a list of components of type <typeparamref name="T"/> produced by a manufacturer.
        /// </summary>
        protected abstract IReadOnlyList<T> GetComponentList(IManufacturer mfr);

        /// <summary>
        /// Returns a string representation of the <paramref name="component"/>'s type (or name), derived based on the
        /// list of vehicle revisions that use the component during a specific model year.
        /// </summary>
        protected abstract string GetComponentTypeString(T component, List<IVehicleRevision> vehRevisions);

        /// <summary>
        /// Returns a component revision object appropriate for a <paramref name="component"/> of type
        /// <typeparamref name="T"/>, which is used by a specific <paramref name="vehicle"/> during a given model
        /// <paramref name="year"/>.
        /// </summary>
        protected abstract IOEMComponentRevision GetComponentRevision(T component, IVehicle vehicle, int year);

        private List<ComponentReportData> GetReportData(int year, T component, IEnumerable<IVehicle> vehs)
        {
            // Build lists of vehicles associated with revised component.
            var allVehicles = new List<IVehicleRevision>();
            var componentRevisionVehicles = new Dictionary<IOEMComponentRevision, List<IVehicleRevision>>();

            foreach (var veh in vehs) {
                var componentRevision = this.GetComponentRevision(component, veh, year);
                if (componentRevision == null) {
                    continue;
                }

                if (!componentRevisionVehicles.ContainsKey(componentRevision)) {
                    componentRevisionVehicles.Add(componentRevision, new List<IVehicleRevision>());
                }

                var vehRevision = veh.GetRevision(year);
                componentRevisionVehicles[componentRevision].Add(vehRevision);
                allVehicles.Add(vehRevision);
            }

            // generate report data for each component revision
            var allReportData = new List<ComponentReportData>();
            foreach (var item in componentRevisionVehicles) {
                var reportData = new ComponentReportData(this, year, component, item.Key, item.Value);
                allReportData.Add(reportData);
            }

            // if several component revisions exist generate report data for "Multiple" component version
            if (allReportData.Count != 1) {
                var reportData = new ComponentReportData(this, year, component, null, allVehicles);
                allReportData.Add(reportData);
            }

            return allReportData;
        }

        private object[] GetValues(ComponentReportData reportData)
        {
            this.ClearBuffer();

            this.AppendHeaderValues(reportData);
            this.AppendDesignAndLeaderInfo(reportData);
            this.AppendAggregateVehicleInfo(reportData);

            return this.GetBuffer();
        }

        private void AppendHeaderValues(ComponentReportData reportData)
        {
            this.AppendBuffer(reportData.ScenarioIndex);
            this.AppendBuffer(reportData.ScenarioName);
            this.AppendBuffer(reportData.ModelYear);
            this.AppendBuffer(reportData.Manufacturer);
            this.AppendBuffer(reportData.Code);
            this.AppendBuffer(reportData.ComponentVersion);
            this.AppendBuffer(reportData.ComponentType);
        }

        private void AppendDesignAndLeaderInfo(ComponentReportData reportData)
        {
            this.AppendBuffer(reportData.DesignState);

            if (!reportData.HasVehicles) {
                this.AppendEmpty(3);
            }
            else if (reportData.LeaderCode == -1) {
                this.AppendBuffer("N/A", 3);
            }
            else {
                this.AppendBuffer(reportData.LeaderCode);
                this.AppendBuffer(reportData.LeaderSales);
                this.AppendCostValue(reportData.LeaderMsrp);
            }
        }

        private void AppendAggregateVehicleInfo(ComponentReportData reportData)
        {
            if (!reportData.HasVehicles) {
                this.AppendEmpty(NumAggregateVehInfoValues);
                return;
            }

            var baselineData = reportData.BaselineData;
            var revisionData = reportData.RevisionData;

            // sales & msrp
            this.AppendBuffer(baselineData.Sales);
            this.AppendBuffer(revisionData.Sales);
            this.AppendCostValue(baselineData.AvgMsrp);
            this.AppendCostValue(revisionData.AvgMsrp);

            // veh attributes
            void AppendWeightHelper(double baselineValue, double revisionValue)
            {
                this.AppendWeightValue(baselineValue);
                this.AppendWeightValue(revisionValue);
            }

            AppendWeightHelper(baselineData.AvgCurbWeight, revisionData.AvgCurbWeight);
            AppendWeightHelper(baselineData.AvgTestWeight, revisionData.AvgTestWeight);
            AppendWeightHelper(baselineData.AvgGvwr, revisionData.AvgGvwr);
            AppendWeightHelper(baselineData.AvgGcwr, revisionData.AvgGcwr);

            this.AppendFootprintValue(baselineData.AvgFootprint);
            this.AppendFootprintValue(revisionData.AvgFootprint);

            // fuel economy info
            this.AppendVehRating(baselineData.AvgRatedFE);
            this.AppendVehRating(revisionData.AvgRatedFE);
            this.AppendVehRating(baselineData.AvgComplianceFE);
            this.AppendVehRating(revisionData.AvgComplianceFE);

            // tech utilization
            this.AppendBuffer(baselineData.TechKeyString);
            this.AppendBuffer(revisionData.TechKeyString);
        }

        /// <summary>
        /// Defines report data for a single row/record.
        /// </summary>
        [Serializable]
        private sealed class ComponentReportData
        {
            public ComponentReportData(
                ComponentReportDataGeneratorBase<T> parent, int year, T component,
                IOEMComponentRevision componentRevision, List<IVehicleRevision> vehRevisions)
            {
                this.HasVehicles = vehRevisions.Count > 0;

                var scen = parent.Scenario;
                this.ScenarioIndex = scen.Index;
                this.ScenarioName = StringHelper.GetTitleCase(scen.Description);
                this.ModelYear = year;
                this.Manufacturer = component.ManufacturerName;

                this.Code = component.Code;

                this.ComponentVersion = (!this.HasVehicles) ? "Obsolete"
                    : (componentRevision == null) ? "Multiple"
                    : (componentRevision.RevisionYear == parent.Data.MinYear) ? "baseline"
                    : $"v.{componentRevision.RevisionYear}";

                this.ComponentType = (this.HasVehicles)
                    ? parent.GetComponentTypeString(component, vehRevisions)
                    : "Obsolete";

                this.DesignState = component.GetRedesignRefreshString(year);

                this.LeaderCode = component.BaselineInfo.LeaderCode;
                if (this.LeaderCode != -1) {
                    var leaderRevision = component.Industry.FindVehicleByCode(this.LeaderCode).Revisions[year];
                    this.LeaderSales = leaderRevision.Sales;
                    this.LeaderMsrp = leaderRevision.Msrp + leaderRevision.ComplianceCosts.RegCost;
                }

                // we can't calculate attribute data if there are no vehicles
                if (this.HasVehicles) {
                    var settings = parent.Settings;
                    var componentTechs = TechKey.BuildKey(component.TechnologyList);
                    this.BaselineData = new AttributeData(scen, year, true, componentTechs, vehRevisions, settings);
                    this.RevisionData = new AttributeData(scen, year, false, componentTechs, vehRevisions, settings);
                }
            }

            public int ScenarioIndex { get; }
            public string ScenarioName { get; }
            public int ModelYear { get; }
            public string Manufacturer { get; }

            // component info
            public int Code { get; }
            public string ComponentVersion { get; }
            public string ComponentType { get; }
            public string DesignState { get; }

            // component's candidate leader vehicle (if exists)
            public int LeaderCode { get; }
            public double LeaderSales { get; }
            public double LeaderMsrp { get; }

            /// <summary>
            /// Gets whether the component has any vehicles during the model year being reported. For "obsolete"
            /// components (e.g., if all vehicles went HEV+), this value will be null.
            /// </summary>
            public bool HasVehicles { get; }

            /// <summary>
            /// Gets the sales-weighted attribute data based on the baseline engineering characteristics of one or more
            /// vehicle models.
            /// </summary>
            public AttributeData BaselineData { get; }

            /// <summary>
            /// Gets the sales-weighted attribute data based on the "final" revision information of engineering
            /// characteristics of one or more vehicle models during a specific model year.
            /// </summary>
            public AttributeData RevisionData { get; }

        }

        /// <summary>
        /// Defines the sales-weighted attribute data for one or more vehicle models.
        /// </summary>
        [Serializable]
        private class AttributeData
        {
            public AttributeData(
                Scenario scen, int year, bool useBaselineInfo, TechKey componentLevelTechs,
                List<IVehicleRevision> vehRevisions, ModelingSettings settings)
            {
                this.IsBaselineInfo = useBaselineInfo;

                foreach (var vehRevision in vehRevisions) {
                    IVehicleInfo vehInfo = this.AsVehicleInfo(vehRevision);
                    double regCost = (useBaselineInfo) ? 0 : vehRevision.ComplianceCosts.RegCost;

                    this.Sales += vehInfo.Sales;
                    this.AvgMsrp += vehInfo.Sales * (vehInfo.Msrp + regCost);
                    this.AvgCurbWeight += vehInfo.Sales * vehInfo.CurbWeight;
                    this.AvgTestWeight += vehInfo.Sales * vehInfo.GetTestWeight();
                    this.AvgGvwr += vehInfo.Sales * vehInfo.GVWR;
                    this.AvgGcwr += vehInfo.Sales * vehInfo.GCWR;
                    this.AvgFootprint += vehInfo.Sales * vehInfo.Footprint;

                    double avgFE = Standards.GetAverageVehicleFERating(scen, year, vehInfo, settings, false);
                    double avgAdjFE = Standards.GetAverageVehicleFERating(scen, year, vehInfo, settings, true);

                    this.AvgRatedFE += vehInfo.Sales / avgFE;
                    this.AvgComplianceFE += vehInfo.Sales / avgAdjFE;
                }

                if (this.HasSales) {
                    this.AvgMsrp /= this.Sales;

                    this.AvgCurbWeight /= this.Sales;
                    this.AvgTestWeight /= this.Sales;
                    this.AvgGvwr /= this.Sales;
                    this.AvgGcwr /= this.Sales;
                    this.AvgFootprint /= this.Sales;

                    this.AvgRatedFE = this.Sales / this.AvgRatedFE;
                    this.AvgComplianceFE = this.Sales / this.AvgComplianceFE;

                    this.TechKeyString = this.CalcTechKeyString(vehRevisions, componentLevelTechs);
                }
            }

            public bool IsBaselineInfo { get; }
            public bool HasSales => this.Sales > 0;

            public double Sales { get; }
            public double AvgMsrp { get; }
            public double AvgCurbWeight { get; }
            public double AvgTestWeight { get; }
            public double AvgGvwr { get; }
            public double AvgGcwr { get; }
            public double AvgFootprint { get; }
            public double AvgRatedFE { get; }
            public double AvgComplianceFE { get; }
            public string TechKeyString { get; }

            private IVehicleInfo AsVehicleInfo(IVehicleRevision vehRevision)
            {
                return (this.IsBaselineInfo) ? vehRevision.BaselineInfo : (IVehicleInfo)vehRevision;
            }

            private string CalcTechKeyString(List<IVehicleRevision> vehRevisions, TechKey componentLevelTechs)
            {
                var techKeys = GetTechKeys();
                return (techKeys.Count == 1)
                    ? techKeys[0].Substring(1, techKeys[0].Length - 2)
                    : string.Join(", ", techKeys);

                List<string> GetTechKeys()
                {
                    var keys = vehRevisions.ConvertAll(vehRevision => {
                        IVehicleInfo vehInfo = this.AsVehicleInfo(vehRevision);
                        return $"{{{TI.KeyToString(vehInfo.TechKey & componentLevelTechs)}}}";
                    });
                    return keys.Distinct().ToList();
                }
            }

        }

    }
}
