using System;

namespace Volpe.Cafe
{
    /// <summary>
    /// Represents an object that stores modeling data for each fuel type.
    /// <br/>
    /// This object supports and operates on all fuel types as defined by <see cref="FuelTypes.List"/>.
    /// </summary>
    [Serializable]
    public struct FuelValue
    {
        #region <<< Constructors >>>

        FuelValue(double value) : this(value, value, value, value, value, value) { }
        FuelValue(double gasoline, double ethanol85, double diesel, double electricity, double hydrogen, double cng)
            : this()
        {
            this.Gasoline    = gasoline;
            this.Ethanol85   = ethanol85;
            this.Diesel      = diesel;
            this.Electricity = electricity;
            this.Hydrogen    = hydrogen;
            this.CNG         = cng;
        }

        #endregion

        #region <<< Variables >>>

        /// <summary>
        /// Represents the <see cref="FuelValue"/> whose elements are all <see cref="double.NaN"/>. This field is
        /// read-only.
        /// </summary>
        public static readonly FuelValue NaN = new FuelValue(double.NaN);
        /// <summary>
        /// Represents the <see cref="FuelValue"/> whose elements are all 0. This field is read-only.
        /// </summary>
        public static readonly FuelValue Zero = new FuelValue(0D);
        /// <summary>
        /// Represents the <see cref="FuelValue"/> whose elements are all 1. This field is read-only.
        /// </summary>
        public static readonly FuelValue One = new FuelValue(1D);

        #endregion

        #region <<< Properties >>>

        /// <summary>
        /// Gets or sets a <see cref="FuelValue"/> component based on the given <see cref="Cafe.FuelType"/>. When
        /// getting, <see cref="FuelType.All"/> may be used to obtain a total sum of all fuel types; multiple
        /// <see cref="Cafe.FuelType"/> values may also be combined to obtain a sum of those fuel types.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     The fuelType specified does not represent a valid <see cref="Cafe.FuelType"/>.
        ///     This exception is only thrown during a set operation.
        /// </exception>
        public double this[FuelType fuelType]
        {
            get {
                // check for single and "ALL" fuel types first (quicker)
                switch (fuelType) {
                    case FuelType.Gasoline   : return this.Gasoline;
                    case FuelType.Ethanol85  : return this.Ethanol85;
                    case FuelType.Diesel     : return this.Diesel;
                    case FuelType.Electricity: return this.Electricity;
                    case FuelType.Hydrogen   : return this.Hydrogen;
                    case FuelType.CNG        : return this.CNG;
                    case FuelType.All        : return this.Total;
                }

                // check for composite fuel types
                double value = 0;
                foreach (FuelType fuel in FuelTypes.List) {
                    if ((fuelType & fuel) == fuel) {
                        value += this[fuel];
                    }
                }
                return value;
            }
            set {
                switch (fuelType) {
                    case FuelType.Gasoline   : this.Gasoline    = value; break;
                    case FuelType.Ethanol85  : this.Ethanol85   = value; break;
                    case FuelType.Diesel     : this.Diesel      = value; break;
                    case FuelType.Electricity: this.Electricity = value; break;
                    case FuelType.Hydrogen   : this.Hydrogen    = value; break;
                    case FuelType.CNG        : this.CNG         = value; break;

                    default:
                        throw new ArgumentOutOfRangeException(
                            "fuelType",
                            "The value specified does not represent a valid FuelType.");
                }
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="FuelType.Gasoline"/> component of this <see cref="FuelValue"/>.
        /// </summary>
        public double Gasoline { get; set; }
        /// <summary>
        /// Gets or sets the <see cref="FuelType.Ethanol85"/> component of this <see cref="FuelValue"/>.
        /// </summary>
        public double Ethanol85 { get; set; }
        /// <summary>
        /// Gets or sets the <see cref="FuelType.Diesel"/> component of this <see cref="FuelValue"/>.
        /// </summary>
        public double Diesel { get; set; }
        /// <summary>
        /// Gets or sets the <see cref="FuelType.Electricity"/> component of this <see cref="FuelValue"/>.
        /// </summary>
        public double Electricity { get; set; }
        /// <summary>
        /// Gets or sets the <see cref="FuelType.Hydrogen"/> component of this <see cref="FuelValue"/>.
        /// </summary>
        public double Hydrogen { get; set; }
        /// <summary>
        /// Gets or sets the <see cref="FuelType.CNG"/> component of this <see cref="FuelValue"/>.
        /// </summary>
        public double CNG { get; set; }

        /// <summary>Gets the sum of all components of this <see cref="FuelValue"/>.</summary>
        public double Total
        {
            get {
                // check for undefined numbers first
                if (this.IsNaN) { return double.NaN; }
                if (this.IsPositiveInfinity) { return double.PositiveInfinity; }
                if (this.IsNegativeInfinity) { return double.NegativeInfinity; }

                double total = 0;
                foreach (FuelType fuel in FuelTypes.List) {
                    double value = this[fuel];
                    if (!double.IsNaN(value) && !double.IsInfinity(value)) {
                        total += value;
                    }
                }
                return total;
            }
        }

        /// <summary>
        /// Gets whether all components of this <see cref="FuelValue"/> instance are greater than or equal to zero.
        /// </summary>
        public bool IsValid
        {
            get {
                foreach (FuelType fuel in FuelTypes.List) {
                    if (this[fuel] < 0) {
                        return false;
                    }
                }
                return true;
            }
        }
        /// <summary>Gets whether all components of this <see cref="FuelValue"/> instance are zero.</summary>
        public bool IsEmpty => this.Equals(Zero);

        /// <summary>
        /// Gets whether all components of this <see cref="FuelValue"/> instance are <see cref="double.NaN"/>.
        /// </summary>
        public bool IsNaN
        {
            get {
                foreach (FuelType fuel in FuelTypes.List) {
                    if (!double.IsNaN(this[fuel])) {
                        return false;
                    }
                }
                return true;
            }
        }
        /// <summary>
        /// Gets whether all components of this <see cref="FuelValue"/> instance are
        /// <see cref="double.PositiveInfinity"/>.
        /// </summary>
        public bool IsPositiveInfinity
        {
            get {
                foreach (FuelType fuel in FuelTypes.List) {
                    if (!double.IsPositiveInfinity(this[fuel])) {
                        return false;
                    }
                }
                return true;
            }
        }
        /// <summary>
        /// Gets whether all components of this <see cref="FuelValue"/> instance are
        /// <see cref="double.NegativeInfinity"/>.
        /// </summary>
        public bool IsNegativeInfinity
        {
            get {
                foreach (FuelType fuel in FuelTypes.List) {
                    if (!double.IsNegativeInfinity(this[fuel])) {
                        return false;
                    }
                }
                return true;
            }
        }

        /// <summary>
        /// Gets whether this <see cref="FuelValue"/> has positive values for more then one component.
        /// </summary>
        public bool IsMultiFuel
        {
            get {
                int f = (int)this.FuelType;
                return ((f & (f - 1)) != 0);
            }
        }
        /// <summary>Gets the fuel type of all components for this <see cref="FuelValue"/>.</summary>
        public FuelType FuelType
        {
            get {
                FuelType fuelType = FuelType.None;
                foreach (FuelType fuel in FuelTypes.List) {
                    if (this[fuel] != 0) {
                        fuelType |= fuel;
                    }
                }
                return fuelType;
            }
        }

        /// <summary>Gets the primary component for this <see cref="FuelValue"/>.</summary>
        /// <seealso cref="PrimaryFuel"/>
        public double PrimaryValue => this[this.PrimaryFuel];
        /// <summary>
        /// Gets the <see cref="FuelType"/> of the primary component for this <see cref="FuelValue"/>. The primary
        /// component is determined in the following order: Gasoline, Diesel, Electricity, Hydrogen, CNG, Ethanol-85.
        /// </summary>
        public FuelType PrimaryFuel
        {
            get {
                return // primary fuel types
                       (this.Gasoline    != 0) ? FuelType.Gasoline
                     : (this.Diesel      != 0) ? FuelType.Diesel
                     : (this.Electricity != 0) ? FuelType.Electricity
                     : (this.Hydrogen    != 0) ? FuelType.Hydrogen 
                     : (this.CNG         != 0) ? FuelType.CNG
                       // secondary fuel types
                     : (this.Ethanol85   != 0) ? FuelType.Ethanol85
                     :                           FuelType.None;
            }
        }

        #endregion

        #region <<< Methods >>>

        #region < Overloaded Operators >

        /// <summary>Returns a new <see cref="FuelValue"/> that is the sum of the two specified values.</summary>
        public static FuelValue operator +(FuelValue value1, FuelValue value2)
        {
            return new FuelValue(value1.Gasoline    + value2.Gasoline,
                                 value1.Ethanol85   + value2.Ethanol85,
                                 value1.Diesel      + value2.Diesel,
                                 value1.Electricity + value2.Electricity,
                                 value1.Hydrogen    + value2.Hydrogen,
                                 value1.CNG         + value2.CNG);
        }
        /// <summary>Returns a new <see cref="FuelValue"/> that is the sum of the two specified values.</summary>
        public static FuelValue operator +(FuelValue value1, double value2) => value1 + new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the sum of the two specified values.</summary>
        public static FuelValue operator +(double value1, FuelValue value2) => new FuelValue(value1) + value2;
        /// <summary>Returns a new <see cref="FuelValue"/> that is the sum of the two specified values.</summary>
        public static FuelValue operator +(FuelValue value1, int value2) => value1 + new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the sum of the two specified values.</summary>
        public static FuelValue operator +(int value1, FuelValue value2) => new FuelValue(value1) + value2;

        /// <summary>Returns a new <see cref="FuelValue"/> that is the difference of the two specified values.</summary>
        public static FuelValue operator -(FuelValue value1, FuelValue value2)
        {
            return new FuelValue(value1.Gasoline    - value2.Gasoline,
                                 value1.Ethanol85   - value2.Ethanol85,
                                 value1.Diesel      - value2.Diesel,
                                 value1.Electricity - value2.Electricity,
                                 value1.Hydrogen    - value2.Hydrogen,
                                 value1.CNG         - value2.CNG);
        }
        /// <summary>Returns a new <see cref="FuelValue"/> that is the difference of the two specified values.</summary>
        public static FuelValue operator -(FuelValue value1, double value2) => value1 - new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the difference of the two specified values.</summary>
        public static FuelValue operator -(double value1, FuelValue value2) => new FuelValue(value1) - value2;
        /// <summary>Returns a new <see cref="FuelValue"/> that is the difference of the two specified values.</summary>
        public static FuelValue operator -(FuelValue value1, int value2) => value1 - new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the difference of the two specified values.</summary>
        public static FuelValue operator -(int value1, FuelValue value2) => new FuelValue(value1) - value2;

        /// <summary>Returns a new <see cref="FuelValue"/> that is the product of the two specified values.</summary>
        public static FuelValue operator *(FuelValue value1, FuelValue value2)
        {
            return new FuelValue(value1.Gasoline    * value2.Gasoline,
                                 value1.Ethanol85   * value2.Ethanol85,
                                 value1.Diesel      * value2.Diesel,
                                 value1.Electricity * value2.Electricity,
                                 value1.Hydrogen    * value2.Hydrogen,
                                 value1.CNG         * value2.CNG);
        }
        /// <summary>Returns a new <see cref="FuelValue"/> that is the product of the two specified values.</summary>
        public static FuelValue operator *(FuelValue value1, double value2) => value1 * new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the product of the two specified values.</summary>
        public static FuelValue operator *(double value1, FuelValue value2) => new FuelValue(value1) * value2;
        /// <summary>Returns a new <see cref="FuelValue"/> that is the product of the two specified values.</summary>
        public static FuelValue operator *(FuelValue value1, int value2) => value1 * new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the product of the two specified values.</summary>
        public static FuelValue operator *(int value1, FuelValue value2) => new FuelValue(value1) * value2;

        /// <summary>Returns a new <see cref="FuelValue"/> that is the first value divided by the second value.</summary>
        public static FuelValue operator /(FuelValue value1, FuelValue value2)
        {
            return new FuelValue(value1.Gasoline    / value2.Gasoline,
                                 value1.Ethanol85   / value2.Ethanol85,
                                 value1.Diesel      / value2.Diesel,
                                 value1.Electricity / value2.Electricity,
                                 value1.Hydrogen    / value2.Hydrogen,
                                 value1.CNG         / value2.CNG);
        }
        /// <summary>Returns a new <see cref="FuelValue"/> that is the first value divided by the second value.</summary>
        public static FuelValue operator /(FuelValue value1, double value2) => value1 / new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the first value divided by the second value.</summary>
        public static FuelValue operator /(double value1, FuelValue value2) => new FuelValue(value1) / value2;
        /// <summary>Returns a new <see cref="FuelValue"/> that is the first value divided by the second value.</summary>
        public static FuelValue operator /(FuelValue value1, int value2) => value1 / new FuelValue(value2);
        /// <summary>Returns a new <see cref="FuelValue"/> that is the first value divided by the second value.</summary>
        public static FuelValue operator /(int value1, FuelValue value2) => new FuelValue(value1) / value2;

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

        /// <summary>
        /// Determines whether the specified two values are equal.
        /// </summary>
        /// <param name="value1">The first value to compare.</param>
        /// <param name="value2">The second value to compare.</param>
        /// <returns>true, if the two values are equal; false, otherwise.</returns>
        public static bool operator ==(FuelValue value1, FuelValue value2)
        {
            return value1.Equals(value2);
        }
        /// <summary>
        /// Determines whether the specified two values are not equal.
        /// </summary>
        /// <param name="value1">The first value to compare.</param>
        /// <param name="value2">The second value to compare.</param>
        /// <returns>true, if the two values are not equal; false, otherwise.</returns>
        public static bool operator !=(FuelValue value1, FuelValue value2)
        {
            return !value1.Equals(value2);
        }

        #endregion

        #region < Overridden from object >

        /// <summary>
        /// Returns the string representation of this <see cref="FuelValue"/> instance.
        /// </summary>
        /// <returns>The string representation of the <see cref="FuelValue"/> instance.</returns>
        public override string ToString()
        {
            string s = "{";
            foreach (FuelType fuel in FuelTypes.List) {
                s += $"{fuel}={this[fuel]}, ";
            }
            return s.Substring(0, s.Length - 2) + "}";
        }
        /// <summary>
        /// Returns the string representation of this <see cref="FuelValue"/> 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="FuelValue"/> instance.</returns>
        public string ToString(int digits)
        {
            string s = "{";
            foreach (FuelType fuel in FuelTypes.List) {
                s += $"{fuel}={Math.Round(this[fuel], digits)}, ";
            }
            return s.Substring(0, s.Length - 2) + "}";
        }
        /// <summary>
        /// Returns the string representation of this <see cref="FuelValue"/> instance, using the specified format for
        /// all elements, optionally excluding fuel types with zero value.
        /// </summary>
        /// <param name="format">A format string.</param>
        /// <param name="nonZeroOnly">
        ///     true, to exclude fuel types with zero value; false, to include all fuel types.
        /// </param>
        /// <param name="acronyms">true, to use fuel type acronyms; false, to use full fuel type names.</param>
        /// <returns>
        /// The string representation of the <see cref="FuelValue"/> instance, using the specified format.
        /// </returns>
        public string ToString(string format, bool nonZeroOnly, bool acronyms = false)
        {
            FuelType filter = (nonZeroOnly) ? this.FuelType : FuelType.All;
            return this.ToString(format, filter, acronyms);
        }
        /// <summary>
        /// Returns the string representation of this <see cref="FuelValue"/> instance, using the specified format for
        /// all elements, and including fuel types based on the specified filter.
        /// </summary>
        /// <param name="format">A format string.</param>
        /// <param name="filter">A filter indicating the fuel types to include in the string.</param>
        /// <param name="acronyms">true, to use fuel type acronyms; false, to use full fuel type names.</param>
        /// <returns>
        /// The string representation of the <see cref="FuelValue"/> instance, using the specified format.
        /// </returns>
        public string ToString(string format, FuelType filter = FuelType.All, bool acronyms = false)
        {
            if (filter == FuelType.None) {
                filter = this.FuelType;
            }

            string s = string.Empty;
            for (int i = 0; i < FuelTypes.List.Length; i++) {
                FuelType ft = FuelTypes.List[i];
                if ((ft & filter) == ft) {
                    s += (acronyms) ? FuelTypes.Acronyms[i]
                                    : FuelTypes.List[i].ToString();
                    s += $"={this[ft].ToString(format)}, ";
                }
            }
            return (s.Length == 0) ? "{}" : "{" + s.Substring(0, s.Length - 2) + "}";
        }

        /// <summary>
        /// Serves as a hash function for a particular type, suitable for use in hashing algorithms and data structures
        /// like a hash table.
        /// </summary>
        /// <returns>A hash code for the current <see cref="FuelValue"/>.</returns>
        public override int GetHashCode()
        {
            int hash = 0;
            foreach (FuelType fuel in FuelTypes.List) {
                hash ^= this[fuel].GetHashCode();
            }
            return hash;
        }

        /// <summary>
        /// Determines whether the specified <see cref="object"/> is equal to the current instance.
        /// </summary>
        /// <param name="obj">The <see cref="object"/> to compare with the current instance.</param>
        /// <returns>
        /// true, if the specified <see cref="object"/> is equal to the current instance; false, otherwise.
        /// </returns>
        public override bool Equals(object obj)
        {
            return (obj is FuelValue value) ? this.Equals(value) : base.Equals(obj);
        }
        /// <summary>
        /// Determines whether the specified value is equal to the current instance.
        /// </summary>
        /// <param name="value">The value to compare with the current instance.</param>
        /// <returns>true, if the specified value is equal to the current instance; false, otherwise.</returns>
        public bool Equals(FuelValue value)
        {
            foreach (FuelType fuel in FuelTypes.List) {
                if (this[fuel] != value[fuel]) {
                    return false;
                }
            }
            return true;
        }

        #endregion

        /// <summary>
        /// Returns the sum of the specified values.
        /// </summary>
        /// <param name="values">One or more values to sum.</param>
        /// <returns>The sum of the specified values.</returns>
        public static FuelValue Sum(params FuelValue[] values)
        {
            if (values == null || values.Length == 0) {
                return Zero;
            }

            FuelValue sum = Zero;
            foreach (FuelValue value in values) {
                sum.Append(value);
            }
            return sum;
        }

        /// <summary>
        /// Performs a safe division of numerator divided by denominator.
        /// </summary>
        /// <param name="numerator">The value of the numerator.</param>
        /// <param name="denominator">The value of the denominator.</param>
        /// <returns>A new <see cref="FuelValue"/> value that is the numerator divided by the denominator.</returns>
        /// <remarks>
        /// If the values for a specific fuel type in both the numerator and the denominator are zero, the result will
        /// also contain zero for the same fuel type. Otherwise, the division will be carried out as usual. However, if
        /// the numerator for a specific fuel type is not zero, while the denominator is zero, the result for the same
        /// fuel type will be either <see cref="double.PositiveInfinity"/> or <see cref="double.NegativeInfinity"/>.
        /// </remarks>
        public static FuelValue SafeDivide(FuelValue numerator, FuelValue denominator)
        {
            FuelValue value = Zero;
            foreach (FuelType fuel in FuelTypes.List) {
                double n = numerator[fuel];
                double d = denominator[fuel];
                value[fuel] = (n == 0 && d == 0) ? 0 : n / d;
            }
            return value;
        }

        /// <summary>
        /// Appends the specified value to this instance.
        /// </summary>
        /// <param name="value">The value to append to this instance.</param>
        public void Append(FuelValue value)
        {
            this.Gasoline    += value.Gasoline;
            this.Ethanol85   += value.Ethanol85;
            this.Diesel      += value.Diesel;
            this.Electricity += value.Electricity;
            this.Hydrogen    += value.Hydrogen;
            this.CNG         += value.CNG;
        }
        /// <summary>
        /// Appends the specified value, for the specified fuel type, to this instance.
        /// </summary>
        /// <param name="fuelType">The fuel type for which to append the value.</param>
        /// <param name="value">The value to append to this instance.</param>
        public void Append(FuelType fuelType, double value)
        {
            switch (fuelType) {
                case FuelType.Gasoline   : this.Gasoline    += value; break;
                case FuelType.Ethanol85  : this.Ethanol85   += value; break;
                case FuelType.Diesel     : this.Diesel      += value; break;
                case FuelType.Electricity: this.Electricity += value; break;
                case FuelType.Hydrogen   : this.Hydrogen    += value; break;
                case FuelType.CNG        : this.CNG         += value; break;
            }
        }

        /// <summary>
        /// Resets all members of this <see cref="FuelValue"/> instance to their default values (0.0).
        /// </summary>
        public void Clear()
        {
            foreach (FuelType fuel in FuelTypes.List) {
                this[fuel] = 0;
            }
        }

        //----- methods for computing averages -----
        /// <summary>
        /// Computes the arithmetic mean of this fuel value.
        /// </summary>
        public double ArithmeticMean()
        {
            return this.ArithmeticMean(One);
        }
        /// <summary>
        /// Computes the weighted arithmetic mean of this fuel value.
        /// </summary>
        /// <param name="weight">The weight value to use for averaging.</param>
        public double ArithmeticMean(FuelValue weight)
        {
            double result = 0;
            foreach (FuelType fuel in FuelTypes.List) {
                result += weight[fuel] * this[fuel];
            }
            return result / weight.Total;
        }

        /// <summary>
        /// Computes the geometric mean of this fuel value.
        /// </summary>
        public double GeometricMean()
        {
            return this.GeometricMean(One);
        }
        /// <summary>
        /// Computes the weighted geometric mean of this fuel value.
        /// </summary>
        /// <param name="weight">The weight value to use for averaging.</param>
        public double GeometricMean(FuelValue weight)
        {
            double result = 0;
            foreach (FuelType fuel in FuelTypes.List) {
                double value = this[fuel];
                if (value != 0 && !double.IsNaN(value) && !double.IsInfinity(value)) {
                    result += weight[fuel] * Math.Log(value);
                }
            }
            return Math.Exp(result / weight.Total);
        }

        /// <summary>
        /// Computes the harmonic mean of this fuel value.
        /// </summary>
        public double HarmonicMean()
        {
            return this.HarmonicMean(One);
        }
        /// <summary>
        /// Computes the weighted harmonic mean of this fuel value.
        /// </summary>
        /// <param name="weight">The weight value to use for averaging.</param>
        public double HarmonicMean(FuelValue weight)
        {
            double result = 0;
            foreach (FuelType fuel in FuelTypes.List) {
                double value = this[fuel];
                if (value != 0 && !double.IsNaN(value) && !double.IsInfinity(value)) {
                    result += weight[fuel] / value;
                }
            }
            return (result == 0) ? 0 : weight.Total / result;
        }

        #endregion
    }
}
