namespace Ethanol.MalwareSonar.Fuzzy
{
    /// <summary>
    /// Represents a histogram bin-based membership function, where each bin in the histogram
    /// provides a membership value for the values contained within it.
    /// Implements the <see cref="IMembershipFunction"/> interface.
    /// </summary>
    /// <typeparam name="TData">The type of data items used to compute the membership values.</typeparam>
    public class HistogramBinBasedMembership<TData> : IMembershipFunction<TData>
    {
        // An array containing the data items used to compute the membership values.
        private TData[] _values;

        // Array representing the bins of the histogram. Each tuple consists of the bin's center 
        // and the normalized frequency of the bin.
        private Tuple<double, double>[] _bins;

        // Number of bins the histogram is divided into.
        private int _binCount;

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

        /// <summary>
        /// Initializes a new instance of the <see cref="HistogramBinBasedMembership{TData}"/> class.
        /// </summary>
        /// <param name="values">The data items used for membership computation.</param>
        /// <param name="selector">A function to extract the relevant value from a data item.</param>
        /// <param name="binCount">The number of bins to divide the histogram into.</param>
        public HistogramBinBasedMembership(TData[] values, Func<TData, double> selector, int binCount = 10)
        {
            if (values.Length == 0)
                throw new ArgumentException("Values cannot be empty.");

            this._values = values;
            this._binCount = binCount;
            _getValue = selector;

            // Compute histogram bins
            var min = values.Min(selector);
            var max = values.Max(selector);
            var binWidth = (max - min) / binCount;

            _bins = new Tuple<double, double>[binCount];

            for (int i = 0; i < binCount; i++)
            {
                var left = min + i * binWidth;
                var right = left + binWidth;
                var count = values.Count(v => selector(v) >= left && selector(v) < right);
                // Store bin's center and normalized frequency in the bins array
                _bins[i] = new Tuple<double, double>(left + binWidth / 2, count / (double)values.Length);
            }
        }

        /// <summary>
        /// Gets the minimum value from the data.
        /// </summary>
        public double LeftLimit => _values.Min(_getValue);

        /// <summary>
        /// Gets the maximum value from the data.
        /// </summary>
        public double RightLimit => _values.Max(_getValue);

        /// <summary>
        /// Retrieves the membership value of a given element based on its position in the histogram bins.
        /// </summary>
        /// <param name="x">The element for which to retrieve the membership value.</param>
        /// <returns>The membership value corresponding to the histogram bin the element falls into.</returns>
        public double GetMembership(TData x)
        {
            // Determine which bin the input value belongs to
            var binWidth = (RightLimit - LeftLimit) / _binCount;
            int binIndex = (int)((_getValue(x) - LeftLimit) / binWidth);

            // If the value is outside the histogram range, return a membership of 0
            if (binIndex < 0 || binIndex >= _binCount) return 0;

            // Return the bin's normalized frequency as the membership value
            return _bins[binIndex].Item2;
        }

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