/*******************************************************************************
* Copyright (C) 2016 Maxim Integrated Products, Inc., All rights Reserved.
* 
* This software is protected by copyright laws of the United States and
* of foreign countries. This material may also be protected by patent laws
* and technology transfer regulations of the United States and of foreign
* countries. This software is furnished under a license agreement and/or a
* nondisclosure agreement and may only be used or reproduced in accordance
* with the terms of those agreements. Dissemination of this information to
* any party or parties not specified in the license agreement and/or
* nondisclosure agreement is expressly prohibited.
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the name of Maxim Integrated
* Products, Inc. shall not be used except as stated in the Maxim Integrated
* Products, Inc. Branding Policy.
*
* The mere transfer of this software does not imply any licenses
* of trade secrets, proprietary technology, copyrights, patents,
* trademarks, maskwork rights, or any other form of intellectual
* property whatsoever. Maxim Integrated Products, Inc. retains all
* ownership rights.
*******************************************************************************
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Maxim.Charting
{
    public class AutoScaleCalculator : IAutoScaleAxisCalculator
    {
        private const double Hystersis = 0.05;

        private double lastMinimum;
        private double lastMaximum;

        private bool filterEnable = true;
        MedianFilter filter;

        public AutoScaleCalculator(double min, double max)
        {
            lastMinimum = min;
            lastMaximum = max;

            filter = new MedianFilter(8);
        }

        public bool Filter
        {
            get
            {
                return filterEnable;
            }
            set
            {
                filterEnable = value;
            }
        }

        public double Maximum { get; set; }
        public double Minimum { get; set; }

        public int Intervals { get; set; }

        public double LastMaximum
        {
            get
            {
                return lastMaximum;
            }
        }

        public double LastMinimum
        {
            get
            {
                return lastMinimum;
            }
        }

        public double MinimumRange { get; set; }

        public double ScaleTrigger { get; set; }

        public double RescaleTargetRange { get; set; }

        public bool NeedsRescale(double minimum, double maximum)
        {
            double chartRange = lastMaximum - lastMinimum;
            double dataRange = maximum - minimum;

            return (minimum < lastMinimum + chartRange * (0.5 - ScaleTrigger / 2)  // If data moves close to edges of chart
                || maximum > lastMaximum - chartRange * (0.5 - ScaleTrigger / 2)
                || dataRange < chartRange * (0.5 - ScaleTrigger / 2)    // Rescale of AC value delta reaches close the chart range
                || dataRange > chartRange * (0.5 + ScaleTrigger / 2));
        }

        public Tuple<double, double> Interval(double minimum, double maximum)
        {
            double chartRange;
            double dataRange;
            double roundedInterval, roundedIntervalFinal;

            double intervalTarget, center;
            double roundedCenter;

            double roundMin, roundMax;

            chartRange = lastMaximum - lastMinimum;
            dataRange = maximum - minimum;

            if (NeedsRescale(minimum, maximum))
            {
                intervalTarget = (dataRange / (RescaleTargetRange)) / Intervals;
                center = minimum + (maximum - minimum) / 2;
                roundedInterval = roundSignificantDigits(intervalTarget); // Keep MS-digit, drop LS-digits

                /* Special Cases */
                // Exceeds  Minimum and Maximum value
                if (center - roundedInterval * Intervals / 2.0 < Minimum && center + roundedInterval * Intervals / 2.0 > Maximum)
                {
                    intervalTarget = (Maximum - Minimum) / (0.8 * Intervals);
                    center = Minimum + (Maximum - Minimum) / 2;
                }
                else if (roundedInterval * Intervals > (Maximum - Minimum)) // Large Value change, cap to full scale
                {
                    intervalTarget = (maximum - minimum) / (0.8 * Intervals);
                    center = (maximum + minimum) / 2;
                }
                else if (center - roundedInterval * Intervals / 2.0 < Minimum) // Below Minimum
                {
                    intervalTarget = (maximum - Minimum) / (0.8 * Intervals);
                    center = Minimum + (maximum - Minimum) / 2;
                }
                else if (center + roundedInterval * Intervals / 2.0 > Maximum) // Above Maximum
                {
                    intervalTarget = (Maximum - minimum) / (0.8 * Intervals);
                    center = minimum + (Maximum - minimum) / 2;
                }

                // Recalculate interval
                roundedIntervalFinal = roundSignificantDigits(intervalTarget); // Keep MS-digit, drop LS-digits
                if (roundedIntervalFinal < MinimumRange)
                    roundedIntervalFinal = MinimumRange;

                if (Filter == true)
                    roundedIntervalFinal = filter.Filter((int)roundedIntervalFinal);

                //roundedCenter = (((int)center + 100) / 200) * 200; // find center and try to round nicely
                roundedCenter = (((int)center + 50) / 100) * 100;

                roundMin = ((int)(roundedCenter - roundedIntervalFinal * (Intervals / 2.0)));
                roundMax = ((int)(roundedCenter + roundedIntervalFinal * (Intervals / 2.0)));

                lastMinimum = roundMin;
                lastMaximum = roundMax;

                return new Tuple<double, double>(roundMin, roundMax);
            }
            else
            {
                return new Tuple<double, double>(lastMinimum, lastMaximum);
            }
        }

        public Tuple<double, double> Interval(double minimum, double maximum, double chartMinium, double chartMaximum)
        {
            throw new NotImplementedException();
        }

        private double roundSignificantDigits(double value)
        {
            double mag, magPower, magMostSigDigit;

            mag = Math.Floor(Math.Log10(value));
            magPower = Math.Pow(10, mag);

            magMostSigDigit = (int)(value / (magPower) + 0.5);

            if (magMostSigDigit >= 10.0)
                magMostSigDigit = 10;
            else if (magMostSigDigit >= 5.0)
                magMostSigDigit = 5;
            else if (magMostSigDigit >= 2.0)
                magMostSigDigit = 2;
            else
                magMostSigDigit = 1;

            return magMostSigDigit * magPower;
        }
    }

    class MedianFilter
    {
        Queue<int> items;
        int depth;

        public MedianFilter(int depth)
        {
            items = new Queue<int>(depth + 1);
            this.depth = depth;
        }

        public int Filter(int current)
        {
            int last;

            items.Enqueue(current);
            if (items.Count > depth)
            {
                items.Dequeue();
            }

            last = items.Peek();

            if (items.Count < depth)
            {
                return current;
            }
            else
            {
                int count;
                int[] arrayItems = items.ToArray();
                int maxKey = 0;

                Dictionary<int, int> valueOccurance = new Dictionary<int, int>();

                for (int i = 0; i < arrayItems.Length; i++)
                {
                    bool exists = valueOccurance.TryGetValue(arrayItems[i], out count);
                    if (exists)
                    {
                        count++;
                        valueOccurance.Remove(arrayItems[i]);
                        valueOccurance.Add(arrayItems[i], count);
                    }
                    else
                    {
                        valueOccurance.Add(arrayItems[i], 1);
                    }
                }

                foreach (KeyValuePair<int, int> item in valueOccurance)
                {
                    if (item.Key > maxKey)
                    {
                        maxKey = item.Key;
                    }
                }

                return maxKey;
            }

        }
    }
}
