namespace Ethanol.MalwareSonar.Fuzzy
{
    /// <summary>
    /// Represents a histogram-based membership function using a radius approach.
    /// The density of the values around a given point determines the membership value.
    /// </summary>
    /// <typeparam name="TData">The type of data items used to compute the membership values.</typeparam>
    public class HistogramRadiusBasedMembership<TData> : IMembershipFunction<TData>
    {
        // An array containing the data items used to compute the membership values.
        private TData[] _data;

        // The radius parameter determines how close a value needs to be to 
        // the input value to be considered "near" it.
        private double _radius;

        // A function to extract the value from the data item for membership computation.
        private Func<TData, double> _getValue;

        /// <summary>
        /// Gets the sum of density values for all distinct data items.
        /// </summary>
        public double DensitySum { get; init; }

        /// <summary>
        /// Initializes a new instance of the <see cref="HistogramRadiusBasedMembership{TData}"/> class.
        /// </summary>
        /// <param name="data">The data items used for membership computation.</param>
        /// <param name="valueSelector">A function to extract the relevant value from a data item.</param>
        /// <param name="radius">The radius to determine closeness of values.</param>
        public HistogramRadiusBasedMembership(TData[] data, Func<TData, double> valueSelector, double radius = 0.5)
        {
            if (data.Length == 0)
                throw new ArgumentException("Values cannot be empty.");

            this._data = data;
            this._radius = radius;
            this._getValue = valueSelector;

            // Calculate the density sum for all distinct data items.
            foreach (var item in _data.DistinctBy(valueSelector))
            {
                var mr = GetMembershipInternal(valueSelector(item));
                DensitySum += mr;
            }
        }

        /// <summary>
        /// Retrieves the normalized membership value of a given element.
        /// </summary>
        /// <param name="x">The element for which to retrieve the membership value.</param>
        /// <returns>The normalized membership value.</returns>
        public double GetMembership(TData x)
        {
            return GetMembershipInternal(_getValue(x)) / DensitySum;
        }

        /// <summary>
        /// Retrieves the membership value of a given element.
        /// </summary>
        /// <param name="x">The element for which to retrieve the membership value.</param>
        /// <returns>The membership value based on the data density around the element.</returns>
        private double GetMembershipInternal(double x)
        {
            // Count the number of data items within the 'radius' of x.
            var closeValuesCount = _data.Count(v => Math.Abs(_getValue(v) - x) <= _radius);

            // Normalize the count by the total number of data items and 2*radius to get a density estimation.
            var result = closeValuesCount / (_data.Length * 2 * _radius);
            return result;
        }

        public IEnumerable<TData> GetMembers()
        {
            return this._data;
        }
    }
}
