using System;
using System.Linq;
using Volpe.Cafe.Generic;

namespace Volpe.Cafe
{
    /// <summary>
    /// Represents an object that stores modeling data, as <see cref="FuelValue"/>s, for each regulatory class, with
    /// the domestic and imported cars combined into a single passenger car regulatory class.
    /// <br/>
    /// This object supports and operates on all regulatory classes as defined by <see cref="RegClasses.AltList"/> and
    /// all fuel types as defined by <see cref="FuelTypes.List"/>.
    /// </summary>
    [Serializable]
    public class RCAltFuelValue : RCAltValue<FuelValue>, ICloneable
    {
        #region <<< Constructors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="RCAltFuelValue"/> class.
        /// </summary>
        public RCAltFuelValue() : base() { }

        /// <summary>
        /// Initializes a new instance of the <see cref="RCAltFuelValue"/> class using the specified initial value for
        /// all members.
        /// </summary>
        RCAltFuelValue(FuelValue initialValue) : base(() => initialValue) { }

        #endregion

        #region <<< Properties >>>

        //----- static values -----
        /// <summary>Gets the <see cref="RCAltFuelValue"/> value whose elements are all 0.</summary>
        public static RCAltFuelValue Zero => new RCAltFuelValue(FuelValue.Zero);
        /// <summary>Gets the <see cref="RCAltFuelValue"/> value whose elements are all 1.</summary>
        public static RCAltFuelValue One => new RCAltFuelValue(FuelValue.One);

        //----- instance values -----
        /// <summary>
        /// Gets or sets the modeling data value based on the given <see cref="RegulatoryClass"/> and
        /// <see cref="FuelType"/>.
        /// </summary>
        /// <exception cref="IndexOutOfRangeException">
        ///     The regClass specified does not represent a valid <see cref="RegulatoryClass"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The fuelType specified does not represent a valid <see cref="FuelType"/>.
        ///     This exception is only thrown during a set operation.
        /// </exception>
        public double this[RegulatoryClass regClass, FuelType fuelType]
        {
            get
            {
                switch (regClass)
                {
                    case RegulatoryClass.All:
                        return this.Items.Sum(item => item[fuelType]);

                    case RegulatoryClass.AllLightDuty:
                        return base[RegulatoryClass.AllPassengerCar][fuelType]
                             + base[RegulatoryClass.LightTruck][fuelType];

                    default:
                        return base[regClass][fuelType];
                }
            }
            set => this.Items[GetIndex(regClass)][fuelType] = value;
        }
        /// <summary>Gets or sets the modeling data value based on the given <see cref="RegulatoryClass"/>.</summary>
        /// <exception cref="IndexOutOfRangeException">
        ///     The regClass specified does not represent a valid <see cref="RegulatoryClass"/>.
        /// </exception>
        public new FuelValue this[RegulatoryClass regClass]
        {
            get
            {
                switch (regClass)
                {
                    case RegulatoryClass.All         : return this.Total;
                    case RegulatoryClass.AllLightDuty: return this.TotalLD;

                    default:
                        return base[regClass];
                }
            }
            set => base[regClass] = value;
        }

        /// <summary>Gets the sum of the compliance modeling data of all light duty regulatory classes.</summary>
        public FuelValue TotalLD => base[RegulatoryClass.AllPassengerCar] + base[RegulatoryClass.LightTruck];
        /// <summary>Gets the sum of the compliance modeling data of all regulatory classes.</summary>
        public FuelValue Total => this.Items.Aggregate((value1, value2) => value1 + value2);

        #endregion

        #region <<< Methods >>>

        #region < Overloaded Operators >

        /// <summary>Returns a new <see cref="RCAltFuelValue"/> that is the sum of the two specified values.</summary>
        public static RCAltFuelValue operator +(RCAltFuelValue value1, RCAltFuelValue value2)
        {
            RCAltFuelValue result = new RCAltFuelValue();
            for (int i = 0; i < result.Items.Length; i++)
            {
                result.Items[i] = value1.Items[i] + value2.Items[i];
            }
            return result;
        }
        /// <summary>Returns a new <see cref="RCAltFuelValue"/> that is the difference of the two specified values.</summary>
        public static RCAltFuelValue operator -(RCAltFuelValue value1, RCAltFuelValue value2)
        {
            RCAltFuelValue result = new RCAltFuelValue();
            for (int i = 0; i < result.Items.Length; i++)
            {
                result.Items[i] = value1.Items[i] - value2.Items[i];
            }
            return result;
        }
        /// <summary>Returns a new <see cref="RCAltFuelValue"/> that is the product of the two specified values.</summary>
        public static RCAltFuelValue operator *(RCAltFuelValue value1, RCAltFuelValue value2)
        {
            RCAltFuelValue result = new RCAltFuelValue();
            for (int i = 0; i < result.Items.Length; i++)
            {
                result.Items[i] = value1.Items[i] * value2.Items[i];
            }
            return result;
        }
        /// <summary>Returns a new <see cref="RCAltFuelValue"/> that is the first value divided by the second value.</summary>
        public static RCAltFuelValue operator /(RCAltFuelValue value1, RCAltFuelValue value2)
        {
            RCAltFuelValue result = new RCAltFuelValue();
            for (int i = 0; i < result.Items.Length; i++)
            {
                result.Items[i] = value1.Items[i] / value2.Items[i];
            }
            return result;
        }

        /// <summary>Returns a copy of the specified value.</summary>
        public static RCAltFuelValue operator +(RCAltFuelValue value) => value.Clone();
        /// <summary>Returns a negated copy of the specified value.</summary>
        public static RCAltFuelValue operator -(RCAltFuelValue value) => Zero - value;
        /// <summary>Returns a copy of the specified value, incremented by one.</summary>
        public static RCAltFuelValue operator ++(RCAltFuelValue value) => value + One;
        /// <summary>Returns a copy of the specified value, decremented by one.</summary>
        public static RCAltFuelValue operator --(RCAltFuelValue value) => value - One;

        #endregion

        #region < ICloneable Members >

        /// <summary>
        /// Creates a new object that is a copy of the current <see cref="RCAltFuelValue"/> instance.
        /// </summary>
        /// <returns>A new object that is a copy of this <see cref="RCAltFuelValue"/>.</returns>
        object ICloneable.Clone()
        {
            return this.Clone();
        }
        /// <summary>
        /// Creates a copy of the current <see cref="RCAltFuelValue"/> instance.
        /// </summary>
        /// <returns>A new object that is a copy of this <see cref="RCAltFuelValue"/>.</returns>
        public new RCAltFuelValue Clone()
        {
            RCAltFuelValue value = new RCAltFuelValue();
            this.CloneInternal(this, value, null);
            return value;
        }

        #endregion

        /// <summary>
        /// Returns the string representation of this <see cref="RCAltFuelValue"/> instance, using the specified format
        /// for all elements.
        /// </summary>
        /// <param name="format">A format string.</param>
        /// <returns>
        /// The string representation of the <see cref="RCAltFuelValue"/> instance, using the specified format.
        /// </returns>
        public string ToString(string format)
        {
            return GenericsHelper.CalcString(this.Items, RegClasses.AltNames, fv => fv.ToString(format));
        }
        /// <summary>
        /// Returns the string representation of this <see cref="RCAltFuelValue"/> instance, whose elements are rounded
        /// with the specified precision.
        /// </summary>
        /// <param name="digits">The number of fractional digits (precision) in the return value.</param>
        /// <returns>The string representation of the <see cref="RCAltFuelValue"/> instance.</returns>
        public string ToString(int digits)
        {
            return GenericsHelper.CalcString(this.Items, RegClasses.AltNames, fv => fv.ToString(digits));
        }

        /// <summary>
        /// Appends the specified value to this instance.
        /// </summary>
        /// <param name="value">The value to append to this instance.</param>
        public void Append(RCAltFuelValue value)
        {
            for (int rc = 0; rc < value.Items.Length; rc++)
            {
                this.Items[rc].Append(value.Items[rc]);
            }
        }

        //----- methods for computing averages -----
        /// <summary>
        /// Computes the weighted arithmetic mean of this instance for the specified regulatory class and fuel type. If
        /// non-aggregate regulatory classes and fuel types are used, the individual components are returned.
        /// </summary>
        /// <param name="regClass">The regulatory class for which to compute the weighted arithmetic mean.</param>
        /// <param name="fuelType">The fuel type for which to compute the weighted arithmetic mean.</param>
        /// <param name="weight">The weight value to use for averaging.</param>
        /// <returns>
        /// The weighted arithmetic mean, whenever aggregate regulatory classes and/or fuel types are used, or the
        /// individual components for non-aggregate classes and fuel types.
        /// </returns>
        public double CalcArithmeticMean(RegulatoryClass regClass, FuelType fuelType, RCAltFuelValue weight)
        {
            // check for invalid input
            if (regClass == RegulatoryClass.None || fuelType == FuelType.None) { return 0; }

            // calc mean of fuel values across reg-classes
            FuelValue fuelValue = this.CalcArithmeticMean(regClass, weight);

            // for all FT, get mean accross all fuel types; otherwise, get individual FT value
            return (fuelType == FuelType.All)
                 ? fuelValue.ArithmeticMean(weight[regClass])
                 : fuelValue[fuelType];
        }

        /// <summary>
        /// Computes the weighted arithmetic mean of this instance for the specified regulatory class. If non-aggregate
        /// regulatory classes are used, the individual components are returned.
        /// </summary>
        /// <param name="regClass">The regulatory class for which to compute the weighted arithmetic mean.</param>
        /// <param name="weight">The weight value to use for averaging.</param>
        /// <returns>
        /// The weighted arithmetic mean, whenever aggregate regulatory classes are used, or the individual components
        /// for non-aggregate classes.
        /// </returns>
        public FuelValue CalcArithmeticMean(RegulatoryClass regClass, RCAltFuelValue weight)
        {
            if (IsAggregateRC(regClass))
            {
                FuelValue w = weight[regClass];
                FuelValue p = (this * weight)[regClass];
                return SafeDivide(p, w);
            }
            else
            {
                return (regClass == RegulatoryClass.None) ? FuelValue.Zero : this[regClass];
            }
        }

        /// <summary>
        /// Computes the weighted geometric mean of this instance for the specified regulatory class. If non-aggregate
        /// regulatory classes are used, the individual components are returned.
        /// </summary>
        /// <param name="regClass">The regulatory class for which to compute the weighted arithmetic mean.</param>
        /// <param name="weight">The weight value to use for averaging.</param>
        /// <returns>
        /// The weighted geometric mean, whenever aggregate regulatory classes are used, or the individual components
        /// for non-aggregate classes.
        /// </returns>
        public FuelValue CalcGeometricMean(RegulatoryClass regClass, RCAltFuelValue weight)
        {
            if (IsAggregateRC(regClass))
            {
                FuelValue w = weight[regClass];
                FuelValue p = (SafeLog(this) * weight)[regClass];
                return Exp(SafeDivide(p, w));
            }
            else
            {
                return (regClass == RegulatoryClass.None) ? FuelValue.Zero : this[regClass];
            }
        }

        /// <summary>
        /// Computes the weighted harmonic mean of this instance for the specified regulatory class. If non-aggregate
        /// regulatory classes are used, the individual components are returned.
        /// </summary>
        /// <param name="regClass">The regulatory class for which to compute the weighted harmonic mean.</param>
        /// <param name="weight">The weight value to use for averaging.</param>
        /// <returns>
        /// The weighted harmonic mean, whenever aggregate regulatory classes are used, or the individual components
        /// for non-aggregate classes.
        /// </returns>
        public FuelValue CalcHarmonicMean(RegulatoryClass regClass, RCAltFuelValue weight)
        {
            if (IsAggregateRC(regClass))
            {
                FuelValue w = weight[regClass];
                FuelValue p = SafeDivide(weight, this)[regClass];
                return SafeDivide(w, p);
            }
            else
            {
                return (regClass == RegulatoryClass.None) ? FuelValue.Zero : this[regClass];
            }
        }

        // helper methods for computing averages
        static bool IsAggregateRC(RegulatoryClass regClass)
        {
            return regClass == RegulatoryClass.All
                || regClass == RegulatoryClass.AllLightDuty;
        }

        static RCAltFuelValue SafeDivide(RCAltFuelValue numerator, RCAltFuelValue denominator)
        {
            RCAltFuelValue result = Zero;
            for (int i = 0; i < numerator.Items.Length; i++)
            {
                result.Items[i] = SafeDivide(numerator.Items[i], denominator.Items[i]);
            }
            return result;
        }
        static FuelValue SafeDivide(FuelValue numerator, FuelValue denominator)
        {
            FuelValue result = FuelValue.Zero;
            foreach (FuelType fuel in FuelTypes.List)
            {
                double d = denominator[fuel];
                if (d != 0)
                {
                    result[fuel] = numerator[fuel] / d;
                }
            }
            return result;
        }

        static RCAltFuelValue SafeLog(RCAltFuelValue value)
        {
            RCAltFuelValue result = Zero;
            for (int i = 0; i < value.Items.Length; i++)
            {
                result.Items[i] = SafeLog(value.Items[i]);
            }
            return result;
        }
        static FuelValue SafeLog(FuelValue value)
        {
            FuelValue result = FuelValue.Zero;
            foreach (FuelType fuel in FuelTypes.List)
            {
                double d = value[fuel];
                if (d > 0)
                {
                    result[fuel] = Math.Log(d);
                }
            }
            return result;
        }

        static FuelValue Exp(FuelValue value)
        {
            FuelValue result = FuelValue.Zero;
            foreach (FuelType fuel in FuelTypes.List)
            {
                result[fuel] = Math.Exp(value[fuel]);
            }
            return result;
        }

        //----- methods for computing sums (obsolete; to be removed) -----
        /// <summary>
        /// Calculates the sum of multiple components of this instance based on the specified regulatory class.
        /// </summary>
        /// <param name="regClass">The regulatory class for which to calculate the sum.</param>
        /// <returns>
        /// The <see cref="FuelValue"/> that represents the sum of multiple regulatory class, if regClass is
        /// <see cref="RegulatoryClass.All"/> or <see cref="RegulatoryClass.AllLightDuty"/>, or the individual
        /// regulatory class component if regClass is one of the supported regulatory classes.
        /// </returns>
        public FuelValue CalcSum(RegulatoryClass regClass)
        {
            return this[regClass];
        }
        /// <summary>
        /// Calculates the sum of multiple components of this instance based on the specified regulatory class and fuel
        /// type.
        /// </summary>
        /// <param name="regClass">The regulatory class for which to calculate the sum.</param>
        /// <param name="fuelType">The fuel type for which to calculate the sum.</param>
        /// <returns>
        /// The sum of values, whenever aggregate regulatory classes and/or fuel types are used, or the individual
        /// components for non-aggregate classes and fuel types.
        /// </returns>
        public double CalcSum(RegulatoryClass regClass, FuelType fuelType)
        {
            return this[regClass, fuelType];
        }

        #endregion
    }
}
