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

namespace ZeroLevel.Mathemathics
{
    public class HistogramValue
    {
        public int Index { get; internal set; }
        public int Value { get; internal set; }
        public float MinBound { get; internal set; }
        public float MaxBound { get; internal set; }
    }

    public class Histogram
    {
        public HistogramMode Mode { get; }
        public float Min { get; }
        public float Max { get; }
        public float BoundsPeriod { get; }
        public float[] Bounds { get; }
        public int[] Values { get; }

        public Histogram(HistogramMode mode, IEnumerable<float> data, int count = -1)
        {
            Mode = mode;
            Min = data.Min();
            Max = data.Max();
            int M = mode == HistogramMode.LOG ? (int)(1f + 3.2f * Math.Log(data.Count())) : mode == HistogramMode.COUNTS ? count : (int)(Math.Sqrt(data.Count()));
            BoundsPeriod = (Max - Min) / M;
            Bounds = new float[M - 1];

            float bound = Min + BoundsPeriod;
            for (int i = 0; i < Bounds.Length; i++)
            {
                Bounds[i] = bound;
                bound += BoundsPeriod;
            }
            Values = new int[M];
            for (int i = 0; i < Values.Length; i++)
            {
                Values[i] = 0;
            }
            foreach (var v in data)
            {
                if (v < float.Epsilon) continue;
                for (int i = 0; i < Bounds.Length; i++)
                {
                    if (v < Bounds[i])
                    {
                        Values[i]++;
                        break;
                    }
                }
            }
        }

        public float BoundsAverageValue(int index)
        {
            if (index == 0)
            {
                return (Min + Bounds[index]) * 0.5f;
            }
            if (index >= Bounds.Length)
            {
                return (Bounds[Bounds.Length - 1] + Max) * 0.5f;
            }
            return (Bounds[index] + Bounds[index + 1]) * 0.5f;
        }

        public int Count => Values?.Length ?? 0;

        public int CountSignChanges()
        {
            if ((Values?.Length ?? 0) <= 2) return 0;
            int i = 0;
            while (Values![i] <= float.Epsilon) { i++; continue; }
            if ((Values.Length - i) <= 2) return 0;

            var delta = Values[i + 1] - Values[i];
            int changes = 0;
            i++;
            for (; i < Values.Length - 1; i++)
            {
                var d = Values[i + 1] - Values[i];
                if (Math.Abs(d) <= float.Epsilon)
                {
                    continue;
                }
                if (NumbersHasSameSign(d, delta) == false)
                {
                    delta = d;
                    changes++;
                }
            }
            return changes;
        }

        public void Smooth()
        {
            var buffer = new int[Values.Length];
            Array.Copy(Values, buffer, buffer.Length);
            for (int i = 2; i < Values.Length - 3; i++)
            {
                Values[i] = (buffer[i - 2] + buffer[i - 1] + buffer[i] + buffer[i + 1] + buffer[i + 2]) / 5;
            }
        }

        public IEnumerable<HistogramValue> GetMaximums()
        {
            var list = new List<HistogramValue>();

            if ((Values?.Length ?? 0) <= 2) return list;
            int i = 0;
            while (Values![i] <= float.Epsilon) { i++; continue; }
            if ((Values.Length - i) <= 2) return list;

            var delta = Values[i + 1] - Values[i];
            i++;
            for (; i < Values.Length - 1; i++)
            {
                var d = Values[i + 1] - Values[i];
                if (Math.Abs(d) <= float.Epsilon)
                {
                    continue;
                }
                if (NumbersHasSameSign(d, delta) == false)
                {
                    if (delta > 0)
                    {
                        list.Add(new HistogramValue
                        {
                            Index = i,
                            Value = Values[i],
                            MinBound = Bounds[i - 1],
                            MaxBound = Bounds[i]
                        });
                    }
                    delta = d;
                }
            }
            return list;
        }

        public int GetMaximum()
        {
            if ((Values?.Length ?? 0) <= 1) return Values![0]!;
            int maxi = 0;
            int max = 0;
            for (int i = 0; i < (Values?.Length ?? 0); i++)
            {
                if (Values![i] > max)
                {
                    max = Values[i];
                    maxi = i;
                }
            }
            return maxi;
        }

        #region OTSU "https://en.wikipedia.org/wiki/Otsu's_method"
        // function is used to compute the q values in the equation
        private float Px(int init, int end)
        {
            int sum = 0;
            int i;
            for (i = init; i < end; i++)
                sum += Values[i];
            return (float)sum;
        }
        // function is used to compute the mean values in the equation (mu)
        private float Mx(int init, int end)
        {
            int sum = 0;
            int i;
            for (i = init; i < end; i++)
                sum += i * Values[i];

            return (float)sum;
        }
        /*
        public int OTSU()
        {
            float p1, p2, p12;
            int k;
            int threshold = 0;
            float bcv = 0;
            for (k = 0; k < Values.Length; k++)
            {
                p1 = Px(0, k);
                p2 = Px(k + 1, Values.Length);
                p12 = p1 * p2;
                if (p12 == 0)
                    p12 = 1;
                float diff = (Mx(0, k) * p2) - (Mx(k + 1, Values.Length) * p1);
                var test = (float)diff * diff / p12;
                if (test > bcv)
                {
                    bcv = test;
                    threshold = k;
                }
            }
            return threshold;
        }
    */
        /*
1. Градиент V[I] - V[i-1]
2. Походы окнами от 1 и выше, пока не сойдется к бимодальности
3. Найти cutoff как минимум между пиками

Modes =  0
W = 1
D = [V.count1]
Maxes = []
For I in [1..V.count] 
    D= V[I] - V[i-1]
do

Modes =  0
S = +1
do
    for wnd in D
        if wnd.sum > 0 & S < 0
            S = +1
        Elif wnd.sum < 0 & S > 0
            Maxes.push(wnd.maxindex)
            Modes ++
            S = -1
W++
while Modes > 2
If Modes == 2
Cutoff = Maxes[0]
Min = V[I]
For I=Maxes[0] to Maxes[1]
    if V[I] < Min   
        Min = V[I]
        Cutoff = i         
         */

        public int CuttOff()
        {
            if (Values.Length > 1)
            {
                var grad = new int[Values.Length];
                grad[0] = 0;
                grad[1] = 0;
                for (int k = 2; k < Values.Length; k++)
                {
                    grad[k - 1] = Values[k] - Values[k - 1];
                }
                var modes = 0;
                var window = 0;
                var sign = 1;
                var sum = 0;
                var max = 0;
                var maxInd = 0;
                var maxes = new List<int>();
                do
                {
                    maxes.Clear();
                    window++;
                    modes = 0;
                    sum = 0;
                    for (int i = 0; i < grad.Length; i += window)
                    {
                        sum = grad[i];
                        max = Values[i];
                        maxInd = i;
                        for (var w = 1; w < window && (i + w) < grad.Length; w++)
                        {
                            sum += grad[i + w];
                            if (Values[i + w] > max)
                            {
                                max = Values[i + w];
                                maxInd = i + w;
                            }
                        }
                        if (sum > 0 && sign < 0)
                        {
                            sign = 1;
                        }
                        else if (sum < 0 && sign > 0)
                        {
                            modes++;
                            maxes.Add(maxInd);
                            sign = -1;
                        }
                    }
                } while (modes > 2);
                if (modes == 2)
                {
                    var cutoff = maxes[0];
                    var min = Values[cutoff];
                    for (int i = maxes[0] + 1; i < maxes[1]; i++)
                    {
                        if (Values[i] < min)
                        {
                            cutoff = i;
                            min = Values[i];
                        }
                    }
                    return cutoff;
                }
            }
            return -1;
        }

        #endregion

        static bool NumbersHasSameSign(int left, int right)
        {
            return left >= 0 && right >= 0 || left < 0 && right < 0;
        }
    }
}
