// Created with MonoDevelop
//
//    Copyright (C) 2008 Hiperscan GmbH Dresden, Germany
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

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

using Hiperscan.Spectroscopy.Math;
using Hiperscan.Unix;

using MathNet.Numerics;
using MathNet.Numerics.IntegralTransforms;
using MathNet.Numerics.Interpolation;
using MathNet.Numerics.LinearAlgebra.Double;


namespace Hiperscan.Spectroscopy
{

    public partial class DataSet
    {

        public DataSet Filter(int window_size)
        {
            // window size has to be odd
            if (window_size % 2 == 0)
                ++window_size;
            
            if (window_size > this.Count)
                throw new ArgumentException(Catalog.GetString("Window size must not be greater than element number.")); 

            DataSet ds = new DataSet(this.Count);
            
            // apply a rectangular window function (moving average)
            for (int ix=0; ix < this.Count; ++ix)
            {
                double yfi = 0;
                if (ix < window_size/2 || ix >= this.Count-window_size/2-1)
                {
                    yfi = this.Y[ix];
                }
                else
                {
                    for (int jx=-window_size/2; jx <= window_size/2; ++jx)
                    {
                        yfi += this.Y[ix + jx];
                    }
                }

                ds.Add( this.X[ix], yfi/window_size );
            }
            
            return ds;
        }

        public DataSet FirLowpassFilter(double[] coeffs)
        {
            int N = coeffs.Length;

            // add values at beginnig and end of ds
            List<double> x = new List<double>(N);
            List<double> y = new List<double>(N);
            for (int ix=0; ix < N; ++ix)
            {
                x.Add(this.X[this.Count-1] + (double)ix + 1.0);
                y.Add(this.Y[this.Y.Count-1]);
            }
            DataSet after = new DataSet(x, y);

            DataSet ds;
            int count = this.Count;
            if (this.Is1nmEquidistant)
            {
                ds = new DataSet(this);
            }
            else
            {
                int ceil = (int)System.Math.Ceiling(this.X[this.Count-1]);
                List<double> xe = new List<double>(ceil+1);
                for (int xi = (int)System.Math.Floor(this.X[0]); xi <= ceil; ++xi)
                {
                    xe.Add(xi);
                }
                ds = this.Interpolate(xe, InterpolationType.Linear);
                count = xe.Count;
            }
            
            ds.AddRange(after);
            
            ds = ds.Filter(coeffs);
            ds = ds - new DataSet.Operand((double)((N-1)/2), 0.0);
            ds = ds.GetRange((N-1)/2, count);

            return ds;
        }
        
        public DataSet FirBandpassFilter()
        {
            double[] coeffs = DataSet.GetFirBandpassCoefficients();
            int N = coeffs.Length;

            // add zeros at beginnig and end of ds
            List<double> x = new List<double>(N);
            List<double> y = new List<double>(N);
            for (int ix=0; ix < N; ++ix)
            {
                x.Add(this.X[this.Count-1] + (double)ix + 1f);
                y.Add(this.Y[this.Y.Count-1]);
            }
            DataSet after = new DataSet(x, y);
            
            DataSet ds = new DataSet(this);
            ds.AddRange(after);
            
            ds = ds.Filter(coeffs);
            ds = ds - new DataSet.Operand((double)((N-1)/2), 0f);
            ds = ds.GetRange((N-1)/2, this.Count);

            return ds;
        }
        
        public DataSet Filter(double[] b)
        {
            DataSet ds = new DataSet(this.Count);
            
            for (int ix=0; ix < this.Count; ++ix)
            {
                double yfi = 0f;
                for (int jx=0; jx < b.Length; ++jx)
                {
                    if (ix-jx < 0)
                        yfi += this.Y[0] * b[jx];
                    else
                        yfi += this.Y[ix-jx] * b[jx];
                }
                
                ds.Add(this.X[ix], yfi);
            }
            
            return ds;
        }
        
        public DataSet Filter(int order, int window_size)
        {
            // window size has to be odd
            if (window_size % 2 == 0)
                ++window_size;

            if (window_size > this.Count)
                throw new ArgumentException(Catalog.GetString("Window size must not be greater than element number.")); 

            Matrix FIR = SavitzkyGolay.FIR(order, window_size);
            DataSet ds = new DataSet((window_size+1)/2-1);

            // transient on
            for (int ix=0; ix < (window_size+1)/2-1; ++ix)
            {
                double yfi = 0f;
                for (int jx=0; jx < window_size; ++jx)
                {
                    yfi += this.Y[jx] * FIR[window_size-ix-1, window_size-jx-1];
                }
                
                ds.Add(this.X[ix], yfi);
            }
            
            // steady state
            double[] b  = FIR.Row((window_size+1)/2-1).ToArray();
            DataSet ds_fi = this.Filter(b);
            ds_fi.RemoveRange(0, window_size-1);
            ds_fi = ds_fi - new Operand((double)((window_size+1)/2-1), 0f);
            ds.AddRange(ds_fi);
            
            // transient off
            int lx = 0;
            for (int ix=this.Count-(window_size+1)/2+1; ix < this.Count; ++ix)
            {
                double yfi = 0f;
                for (int jx=0; jx < window_size; ++jx)
                {
                    yfi += this.Y[this.Count-jx-1] * FIR[(window_size+1)/2-2-lx, jx];
                }
                ++lx;
                
                ds.Add(this.X[ix], yfi);
            }

            return ds;
        }

        public DataSet Differences()
        {
            DataSet ds = new DataSet(this.Count-1);
            
            for (int ix=0; ix < this.Count-1; ++ix)
            {
                ds.Add( this.X[ix], this.Y[ix+1] - this.Y[ix] );
            }
            
            return ds;
        }
        
        public DataSet Derivative(int polynomial_order, int window_size, int diff_order)
        {
            // window size has to be odd
            if (window_size % 2 == 0)
                ++window_size;
            
            if (window_size > this.Count)
                throw new ArgumentException(Catalog.GetString("Window size must not be greater than element number."));
            
            Matrix SG = SavitzkyGolay.SG(polynomial_order, window_size);

            if (diff_order > SG.ColumnCount)
                throw new ArgumentException(Catalog.GetString("Derivative order must not be greater than polynomial order."));

            DataSet ds = new DataSet(this.Count-window_size+1);
            
            double dx = this.X[1] - this.X[0];
            
            for (int ix=0; ix < this.Count-window_size+1; ++ix)
            {
                Vector v = new DenseVector(window_size);
                for (int jx=0; jx < window_size; ++jx)
                {
                    v[jx] = this.Y[ix+jx];
                }
                
                double difference = SG.Column(diff_order) * v;
                double derivative = SpecialFunctions.Factorial(diff_order) * difference / System.Math.Pow(dx, (double)diff_order);
                
                ds.Add(this.X[ix+(window_size+1)/2-1], derivative);
            }
            
            return ds;
        }
        
        public static DataSet Sign(DataSet ds)
        {
            List<double> y = new List<double>(ds.Count);
            
            for (int ix=0; ix < ds.Count; ++ix)
            {
                y.Add(ds.Y[ix] >= 0 ? 1 : -1);
            }

            return new DataSet(new List<double>(ds.X), y);
        }
        
        public static DataSet Ln(DataSet ds)
        {
            List<double> y = new List<double>(ds.Count);
            
            for (int ix=0; ix < ds.Count; ++ix)
            {
                y.Add(System.Math.Log(ds.Y[ix]));
            }

            return new DataSet(new List<double>(ds.X), y);            
        }
        
        public static DataSet Log(DataSet ds)
        {
            List<double> y = new List<double>(ds.Count);
            
            for (int ix=0; ix < ds.Count; ++ix)
            {
                y.Add(System.Math.Log10(ds.Y[ix]));
            }

            return new DataSet(new List<double>(ds.X), y);            
        }
        
        public static DataSet Pow(double basis, DataSet ds)
        {
            List<double> y = new List<double>(ds.Count);
            
            for (int ix=0; ix < ds.Count; ++ix)
            {
                y.Add(System.Math.Pow(basis, ds.Y[ix]));
            }

            return new DataSet(new List<double>(ds.X), y);            
        }
        
        public static DataSet Pow(DataSet ds, double exponent)
        {
            List<double> y = new List<double>(ds.Count);
            
            for (int ix=0; ix < ds.Count; ++ix)
            {
                y.Add(System.Math.Pow(ds.Y[ix], exponent));
            }

            return new DataSet(new List<double>(ds.X), y);            
        }
        
        public static double AbsorbanceLimit(DataSet reference, DarkIntensity di)
        {
            return System.Math.Log10(reference.YMax() / (3* di.Sigma));
        }
        
        public static DataSet Absorbance(DataSet intensity, DataSet background, DarkIntensity di)
        {
            DataSet ratio = background.Interpolate(intensity.X) / intensity;
            double max_absorbance = DataSet.AbsorbanceLimit(background, di);
            double max_ratio = System.Math.Pow(10.0, max_absorbance);
            List<int> indices = ratio.Find('y', '<', double.Epsilon);
            indices.AddRange(ratio.Find('y', '>', max_ratio));
            
            foreach (int ix in indices)
            {
                ratio.Y[ix] = max_ratio;
            }

            DataSet ds  = DataSet.Log(ratio);
            ds.DataType = DataType.Absorbance;
            
            return ds;
        }

        public static DataSet Reflectance(DataSet intensity, DataSet reference)
        {
            DataSet ds = intensity / reference.Interpolate(intensity.X);
            ds.DataType = DataType.Reflectance;

            return ds;
        }
        
        public static DataSet InverseAbsorbance(DataSet intensity, DataSet absorbance)
        {
            DataSet ds = DataSet.Pow(10.0, absorbance) * intensity;
            ds.DataType = DataType.Reference;
            
            return ds;
        }
        
        public void YShift(int shift)
        {
            List<double> shifted_y = new List<double>(this.Count);
            
            for (int ix=0; ix < this.Count; ++ix)
            {
                int jx = ix - shift;
                
                while (jx < 0)
                    jx += this.Count;
                
                jx %= this.Count;

                shifted_y.Add(this.Y[jx]);
            }
            
            this.Y = shifted_y;
        }

        public double XMedian()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine x median value: Not enough data available."));

            List<double> x = new List<double>(this.X);
            x.Sort();
            return (x[0] + x[x.Count-1]) / 2;
        }

        public double XMin()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine x minimum value: Not enough data available."));

            if (double.IsNaN(this.__xmin))
            {
                List<double> x = new List<double>(this.X);
                x.Sort();
                this.__xmin = x[0];
                this.__xmax = x[x.Count-1];
            }

            return this.__xmin;
        }
        
        public static double Mean(IEnumerable<double> X)
        {
            double sum = 0.0;
            
            foreach (double x in X)
            {
                if (double.IsNaN(x))
                    continue;
                
                sum += x;
            }

            return sum / (double)X.Count();
        }

        public static DataSet Mean(IEnumerable<DataSet> data_sets)
        {
            List<DataSet> dss = data_sets.ToList();

            if (dss == null || dss.Count < 1)
                throw new ArgumentException(Catalog.GetString("Cannot calculate mean data set for an empty list."));

            DataSet mean_ds = dss[0];
            for (int ix=1; ix < dss.Count; ++ix)
            {
                mean_ds += dss[ix].Interpolate(mean_ds.X);
            }

            return mean_ds / dss.Count;
        }
        
        public static double Median(List<double> X)
        {
            List<double> x_sort = new List<double>(X);
            x_sort.Sort();
            int count = x_sort.Count;
            
            if (count%2 == 1)
                return x_sort[(count-1)/2];
            else
                return (x_sort[count/2-1] + x_sort[count/2]) / 2.0;
        }
        
        public double XMean()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine x mean value: Not enough data available."));

            return DataSet.Mean(this.X);
        }
        
        public double YMean()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine y mean value: Not enough data available."));

            return DataSet.Mean(this.Y);
        }
        
        public double YMedian()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine y median value: Not enough data available."));

            return DataSet.Median(this.Y);
        }
        
        public static double Rms(List<double> X)
        {
            double sum = 0.0;
            
            foreach (double x in X)
            {
                if (double.IsNaN(x))
                    continue;
                
                sum += System.Math.Pow(x, 2f);
            }

            return System.Math.Sqrt( sum / (double)X.Count );
        }
        
        public double YRms()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine y RMS value: Not enough data available."));

            return DataSet.Rms(this.Y);
        }
        
        public static double Sigma(IEnumerable<double> X)
        {
            double xmean = DataSet.Mean(X);
            double N     = (double)X.Count();
            double sum   = 0.0;
            
            foreach (double x in X)
            {
                if (double.IsNaN(x))
                    continue;

                sum += System.Math.Pow( (x - xmean), 2.0);
            }
            
            return System.Math.Sqrt(sum / N);
        }
        
        public double XSigma()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine x sigma value: Not enough data available."));

            return DataSet.Sigma(this.X);
        }

        public double YSigma()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine y sigma value: Not enough data available."));

            return DataSet.Sigma(this.Y);
        }

        public double YMin()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine y minimum value: Not enough data available."));

            if (double.IsNaN(this.__ymin))
            {
                List<double> y = new List<double>(this.Y);
                y.Sort();
                for (int ix=0; ix < y.Count; ++ix)
                {
                    if (double.IsNaN(y[ix]) || double.IsNegativeInfinity(y[ix]))
                        continue;
                    this.__ymin = y[ix];
                    break;
                }
            }

            if (double.IsNaN(this.__ymin))
                throw new NotFiniteNumberException(Catalog.GetString("Y-data contains non-finite values only."));

            if (double.IsNaN(this.y_min))
                return this.__ymin;

            return (this.y_min < this.__ymin) ? this.y_min : this.__ymin;
        }

        public double XMax()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine x maximum value: Not enough data available."));

            if (double.IsNaN(this.__xmax))
            {
                List<double> x = new List<double>(this.X);
                x.Sort();
                this.__xmin = x[0];
                this.__xmax = x[x.Count-1];
            }
            return this.__xmax;
        }

        public double YMax()
        {
            if (this.IsValid == false)
                throw new Exception(Catalog.GetString("Cannot determine y minimum value: Not enough data available."));

            if (double.IsNaN(this.__ymax))
            {
                List<double> y = new List<double>(this.Y);
                y.Sort();
                this.__ymax = y[y.Count-1];
            }

            if (double.IsNaN(this.__ymax))
                throw new NotFiniteNumberException(Catalog.GetString("Y-data contains NaN values only"));

            if (double.IsNaN(this.y_max))
                return this.__ymax;

            return (this.y_max > this.__ymax) ? this.y_max : this.__ymax;
        }
                
        public static DataSet Abs(DataSet ds)
        {
            return new DataSet(new List<double>(ds.X), DataSet.Abs(ds.Y));
        }

        public static List<double> Abs(List<double> list)
        {
            List<double> y = new List<double>(list.Count);

            for (int ix=0; ix < list.Count; ++ix)
            {
                y.Add(System.Math.Abs(list[ix]));
            }

            return y;
        }

        public double Interpolate(double xi)
        {
            DataSet ds = this.Interpolate(new double[] { xi });
            return ds.Y[0];
        }
        
        public DataSet Interpolate(double[] xi)
        {
            return this.Interpolate(xi, InterpolationType.Linear);
        }
        
        public DataSet Interpolate(List<double> xi)
        {
            return this.Interpolate(xi, InterpolationType.Linear);
        }
        
        public DataSet Interpolate(double[] xi, InterpolationType type)
        {
            IInterpolation data;
            
            switch (type)
            {
                
            case InterpolationType.Linear:
                data = LinearSpline.Interpolate(this.X, this.Y);
                break;
                
            case InterpolationType.NaturalCubic:
                data = CubicSpline.InterpolateNatural(this.X, this.Y);
                break;
                
            default:
                throw new Exception(Catalog.GetString("Unknown interpolation type:") +
                                    " " + Enum.GetName(typeof(InterpolationType), type));
                
            }
                
            DataSet ds = new DataSet(xi.Length);
            foreach (double x in xi)
            {
                ds.Add(x, data.Interpolate(x));
            }
            
            return ds;
        }

        public DataSet Interpolate(List<double> xi, InterpolationType type)
        {
            return Interpolate(xi.ToArray(), type);
        }
        
        public DataSet ApplyLUT(DataSet LUT)
        {
            IInterpolation data = LinearSpline.Interpolate(LUT.X, LUT.Y);
            DataSet ds = new DataSet(this.Count);
            
            for (int ix=0; ix < this.Count; ++ix)
            {
                ds.Add( data.Interpolate(this.X[ix]), this.Y[ix] );
            }
            return ds;
        }
        
        public double InterpolateMinimum(double a0, double b0, double eps)
        {
            double tau = 0.618;
            double   ai = a0;
            double   bi = b0;
            double[] p  = new double[2];
            p[0] = ai + (1-tau) * (bi-ai);
            p[1] = ai + tau * (bi-ai);
            
            for (int ix=0; true; ++ix)
            {
                if (ix > 100)
                    throw new OperationCanceledException(string.Format(Catalog.GetString("The maximum number of iterations ({0}) has been reached."),
                                                                       100));
                
                DataSet f = this.Interpolate(p, InterpolationType.NaturalCubic);
                
                if (f.Y[0] < f.Y[1])
                {
                    bi   = p[1];
                    p[1] = p[0];
                    p[0] = ai + (1-tau) * (bi-ai);
                }
                else
                {
                    ai   = p[0];
                    p[0] = p[1];
                    p[1] = ai + tau * (bi-ai);
                }
                
                if (bi-ai < eps)
                    break;
            }
            
            return ai;
        }
        
        public List<int> Find(char xy, char op, double val)
        {
            List<int> indices = new List<int>();

            List<double> operand;
            
            switch (xy)
            {

            case 'x':
            case 'X':
                 operand = this.X;
                break;
                
            case 'y':
            case 'Y':
                operand = this.Y;
                break;
                
            default:
                throw new ArgumentException(Catalog.GetString("Unknown operand:") + " '" + xy + "'");

            }

            switch (op)
            {

            case '=':
                // find indices at operand crossing val
                for (int ix = 1; ix < this.Count; ++ix)
                {
                    if ((operand[ix - 1] - val) * (operand[ix] - val) < 0)
                        indices.Add(ix);
                }
                break;

            case '<':
                // find indices for operand < val
                for (int ix = 0; ix < this.Count; ++ix)
                {
                    if (operand[ix] < val)
                        indices.Add(ix);
                }
                break;

            case '>':
                // find indices for operand > val
                for (int ix = 0; ix < this.Count; ++ix)
                {
                    if (operand[ix] > val)
                        indices.Add(ix);
                }
                break;

            default:
                throw new ArgumentException(Catalog.GetString("Unknown operator:") + " '" + op + "'");

            }

            return indices;
        }
        
        public List<int> Find(char xy, char op1, double val1, char op2, double val2)
        {
            List<int> indices = new List<int>();

            List<double> operand;
            
            switch (xy)
            {

            case 'x':
            case 'X':
                 operand = this.X;
                break;
                
            case 'y':
            case 'Y':
                operand = this.Y;
                break;
                
            default:
                throw new ArgumentException(Catalog.GetString("Unknown operand:") + " '" + xy + "'");

            }
            
            switch (op1)
            {
                
            case '<':
                // find indices for operand < val1
                for (int ix=0; ix < this.Count; ++ix)
                {
                    if ( operand[ix] < val1)
                        indices.Add(ix);
                }
                break;
                
            case '>':
                // find indices for operand > val1
                for (int ix=0; ix < this.Count; ++ix)
                {
                    if ( operand[ix] > val1)
                        indices.Add(ix);
                }
                break;
                
            default:
                throw new ArgumentException(Catalog.GetString("Unknown operator:") + " '" + op1 + "'");

            }
            
            switch (op2)
            {
                
            case '<':
                // find indices for operand < val2
                for (int ix=0; ix < this.Count; ++ix)
                {
                    if ( operand[ix] >= val2 && indices.Contains(ix))
                        indices.Remove(ix);
                }
                break;
                
            case '>':
                // find indices for operand > val1
                for (int ix=0; ix < this.Count; ++ix)
                {
                    if ( operand[ix] <= val2 && indices.Contains(ix))
                        indices.Remove(ix);
                }
                break;
                
            default:
                throw new ArgumentException(Catalog.GetString("Unknown operator:") + " '" + op2 + "'");

            }
            
            return indices;
        }
        
        public List<int> FindIndices(DataSet ds)
        {
            List<int> indices = new List<int>();
            
            foreach (double x in ds.X)
            {
                indices.Add(this.X.IndexOf(x));
            }

            return indices;
        }
        
        public DataSet.Extrema FindExtrema(int window_size, double noise_limit, double cutoff, int eps)
        {
            return this.FindExtrema(window_size, noise_limit, cutoff, eps, new LevenbergMarquardt.OptimParams());
        }

        public DataSet.Extrema FindExtrema(int window_size, double noise_limit, double cutoff, int eps, LevenbergMarquardt.OptimParams optim_params)
        {
            // create an instance with "smoother" data
            DataSet ds = this.Filter(window_size);

            // use differences as approximation for differentation
            DataSet ds_diff =  ds.Differences();
            if (ds_diff.Count < 2)
            {
                return null;
            }
            
            // zero crossing indicates extremum
            List<int> indices = ds_diff.Find('Y', '=', 0);            
            DataSet   extrema = ds[indices];
            extrema.IsContinuous = false;            

            // delete extrema caused by noise
            DataSet   extrema_diff_abs = DataSet.Abs(extrema.Differences());
            List<int> indices_del      = extrema_diff_abs.Find('Y', '<', noise_limit);
            if( indices_del.Count > 0 )
                extrema.RemoveAt(indices_del);
            
            // delete extrema beyond cutoff threshold
            indices_del = extrema.Find('Y', '<', cutoff);
            extrema.RemoveAt(indices_del);
            
            // delete extrema too close to the beginning or end
            indices_del.Clear();
            for (int ix=0; ix < extrema.Count; ++ix)
            {
                int n = this.FindNearestIndex(extrema.X[ix]);
                if (n < 10 || n > this.Count-10)
                    indices_del.Add(ix);
            }
            extrema.RemoveAt(indices_del);
            
            // determine wheter it's a minimum or maximum
            indices = ds.FindIndices(extrema);
            DataSet minima = new DataSet(100)
            {
                IsContinuous = false
            };
            DataSet maxima = new DataSet(100)
            {
                IsContinuous = false
            };
            List<int> indices_min = new List<int>();
            List<int> indices_max = new List<int>();
            
            foreach (int ix in indices)
            {
                if (System.Math.Sign(ds.Y[ix] - ds.Y[ix - 1]) < 0 )
                    indices_min.Add(ix);
                else
                    indices_max.Add(ix);
            }

            // find interpolated extrema
            foreach (int ix in indices_min)
            {
                List<int> range = new List<int>();
                for (int jx=ix-eps; jx<=ix+eps; ++jx)
                {
                    if (jx >= 0 && jx < this.Count)
                        range.Add(jx);
                }
                
                DataSet subset = new DataSet(this[range]);
                double[] p;
                try
                {
                    p = LevenbergMarquardt.Fit(new LevenbergMarquardt.ParabolicFit(subset, 1), optim_params).Coefficients;
                }
                catch (LevenbergMarquardt.MaxIterationNumberException)
                {
                    continue;
                }

                double x = p[2];
                double y = p[0] + p[1] * System.Math.Pow(x - p[2], 2); 
                
                minima.Add(x, y);
            }
            
            foreach (int ix in indices_max)
            {
                List<int> range = new List<int>();
                for (int jx=ix-eps; jx<=ix+eps; ++jx)
                {
                    if (jx >= 0 && jx < this.Count)
                        range.Add(jx);
                }
                
                DataSet subset = new DataSet(this[range]);
                double[] p;

                try
                {
                    p = LevenbergMarquardt.Fit(new LevenbergMarquardt.ParabolicFit(subset, -1)).Coefficients;
                }
                catch (LevenbergMarquardt.MaxIterationNumberException)
                {
                    continue;
                }
                
                double x = p[2];
                double y = p[0] + p[1] * System.Math.Pow(x - p[2], 2); 
                
                maxima.Add(x, y);
            }

            return new DataSet.Extrema(minima, maxima);
        }

        public DataSet.Extrema FindExtremaSG(int sg_order, int sg_window, double noise_limit, double cutoff)
        {
            // create an instance with "smoother" data
            DataSet ds = this.Filter(sg_order, sg_window);
            
            // use differences as approximation for differentation
            DataSet ds_diff = ds.Differences();
            
            // zero crossing indicates extremum
            List<int> indices = ds_diff.Find('Y', '=', 0);            
            DataSet   extrema = ds[indices];
            extrema.IsContinuous = false;            

            // delete extrema caused by noise
            List<int> indices_del;
            if (extrema.Count > 2)
            {
                DataSet   extrema_diff_abs = DataSet.Abs(extrema.Differences());
                indices_del = extrema_diff_abs.Find('Y', '<', noise_limit);
                extrema.RemoveAt(indices_del);
            }
            
            // delete extrema beyond cutoff threshold
            indices_del = extrema.Find('Y', '<', cutoff);
            extrema.RemoveAt(indices_del);
            
            // determine wheter it's a minimum or maximum
            indices = ds.FindIndices(extrema);
            DataSet minima = new DataSet(100)
            {
                IsContinuous = false
            };
            DataSet maxima = new DataSet(100)
            {
                IsContinuous = false
            };
            List<int> indices_min = new List<int>();
            List<int> indices_max = new List<int>();
            
            foreach (int ix in indices)
            {
                if (System.Math.Sign(ds.Y[ix] - ds.Y[ix - 1]) < 0 )
                    indices_min.Add(ix);
                else
                    indices_max.Add(ix);
            }

            // find interpolated extrema
            IInterpolation data = LinearSpline.Interpolate(ds.X, ds.Y);
            
            foreach (int ix in indices_min)
            {
                if (ix < 1 || ix > ds.Count-1)
                    continue;
                
                double a0 = ds.X[ix-1];
                double b0 = ds.X[ix+1];
                
                double x = ds.InterpolateMinimum(a0, b0, 1e-3);
                double y = data.Interpolate(x);

                minima.Add(x, y);
            }
            
            ds *= new DataSet.Operand(1.0, -1.0);
            foreach (int ix in indices_max)
            {
                if (ix < 1 || ix > ds.Count-1)
                    continue;
                
                double a0 = ds.X[ix-1];
                double b0 = ds.X[ix+1];
                
                double x = ds.InterpolateMinimum(a0, b0, 1e-3);
                double y = data.Interpolate(x);

                maxima.Add(x, y);
            }

            return new DataSet.Extrema(minima, maxima);
            
        }
        
        public int FindNearestIndex(double x)
        {
            double min_dist = double.MaxValue;
            int min_dist_ix = 0;
            for (int ix=0; ix < this.Count; ++ix)
            {
                if (System.Math.Abs(this.X[ix] - x) < min_dist)
                {
                    min_dist = System.Math.Abs(this.X[ix] - x);
                    min_dist_ix = ix;
                }
            }
            return min_dist_ix;
        }
        
        public double[] FindNearest(double x)
        {
            int ix = this.FindNearestIndex(x);
            return new double[2] { this.X[ix], this.Y[ix] };
        }
        
        public DataSet SNV()
        {
            double mean  = this.YMean();
            double sigma = this.YSigma();

            return (this - new Operand(0.0, mean)) / new Operand(1.0, sigma);
        }

        public double Cov()
        {
            double xmean = this.XMean();
            double ymean = this.YMean();
            double sum   = 0.0;
            
            for (int ix=0; ix < this.Count; ++ix)
            {
                sum += (this.X[ix] - xmean) * (this.Y[ix] - ymean);
            }
            
            return sum / (double)this.Count;
        }
        
        public double Corr()
        {
            return this.Cov() / (this.XSigma() * this.YSigma());
        }
        
        public double Norm2()
        {
            double sum = 0.0;
            
            for (int ix=0; ix < this.Count; ++ix)
            {
                sum += System.Math.Pow( (this.X[ix] - this.Y[ix]), 2f);
            }
            
            return System.Math.Sqrt(sum);
        }

        public double Norm2(List<double> weights)
        {
            if (this.Count != weights.Count)
                throw new ArgumentException(Catalog.GetString("Cannot calculate weighted 2-Norm: Weighting vector has wrong dimension."));
            
            double sum = 0.0;
            for (int ix=0; ix < this.Count; ++ix)
            {
                sum += System.Math.Pow( (this.X[ix] - this.Y[ix]) * weights[ix], 2.0);
            }
            
            return System.Math.Sqrt(sum);
        }
        
        public FwhmResult FWHM()
        {
            Extrema extrema = this.FindExtremaSG(2, 5, 0.05, -10.0);
            
            if (extrema.Maxima.Count == 0)
                throw new Exception(Catalog.GetString("No peak found."));

            double x_peak = 0;
            double y_peak = double.MinValue;
            for (int ix=0; ix < extrema.Maxima.Count; ++ix)
            {
                if (extrema.Maxima.Y[ix] > y_peak)
                {
                    x_peak = extrema.Maxima.X[ix];
                    y_peak = extrema.Maxima.Y[ix];
                }
            }
            double y_median = this.YMedian();
            double limit = (y_peak - y_median) / 2.0 + y_median;
            
            List<int> indices = this.Find('y', '=', limit);
            if (indices.Count < 2)
                throw new Exception(Catalog.GetString("No peak found."));
            
            DataSet cross = this[indices];

            // left hand side
            indices = cross.Find('x', '<', x_peak);
            if (indices.Count < 1)
                throw new Exception(Catalog.GetString("No peak found."));

            double x_left = indices.Count == 1 ? cross.X[indices[0]] : cross[indices].XMax();

            // right hand side
            indices = cross.Find('x', '>', x_peak);
            if (indices.Count < 1)
                throw new Exception(Catalog.GetString("No peak found."));
            double x_right;
            if (indices.Count == 1)
                x_right = cross.X[indices[0]];
            else
                x_right = cross[indices].XMin();
            
            // interpolate peak
            List<double> xi = new List<double>();
            for (double x=x_left - 1.0; x < x_right + 1.0; x += 0.05)
            {
                xi.Add(x);
            }
            DataSet interp = this.Interpolate(xi, InterpolationType.Linear);
            
            indices = interp.Find('y', '=', limit);
            if (indices.Count != 2)
                throw new Exception(Catalog.GetString("No peak found."));
            
            double fhwm = interp.X[indices[1]] - interp.X[indices[0]];
            
            Console.WriteLine("FHWM result:");
            Console.WriteLine("x_peak = " + x_peak);
            Console.WriteLine("y_peak = " + y_peak);
            Console.WriteLine("fwhm = " + fhwm);
            
            return new FwhmResult(x_peak, y_peak, fhwm);
        }
        
        public LinearRegressionResult LinearRegression()
        {
            double xmean = this.XMean();
            double ymean = this.YMean();
            
            double divident = 0.0;
            double divisor  = 0.0;
            
            for (int ix = 0; ix < this.Count; ++ix)
            {
                divident += (this.X[ix] - xmean) * (this.Y[ix] - ymean);
                divisor  += System.Math.Pow(this.X[ix] - xmean, 2.0);
            }
            
            double slope  = divident / divisor;
            double offset = ymean - slope * xmean;
            
            return new LinearRegressionResult(offset, slope);
        }
        
        public DataSet LinearRegressionCurve(LinearRegressionResult regression)
        {
            List<double> x = new List<double>(this.X);
            List<double> y = new List<double>(this.X.Count);
            
            foreach (double val in x)
            {
                y.Add(regression.Offset + val * regression.Slope);
            }
            
            return new DataSet(x, y);
        }

        public DataSet LinearRegressionCurve()
        {
            return this.LinearRegressionCurve(this.LinearRegression());
        }
        
        public DataSet FFT()
        {
            double[] xi = new double[1024];
            
            double delta = (this.X[this.Count-1] - this.X[0]) / (xi.Length - 1);
            
            for (int ix = 0; ix < xi.Length; ++ix)
            {
                xi[ix] = this.X[0] + (double)ix * delta * 2.0;
            }
            
            DataSet ds = this.Interpolate(xi);
            
            List<int> indices = ds.Find('X', '>', this.X[this.Count-1]);
            foreach (int ix in indices)
            {
                ds.Y[ix] = 0.0;
            }
            
            double[] real = ds.Y.ToArray();
            double[] imag = new double[real.Length];
            Fourier.Forward(real, imag);
            
            for (int ix=0; ix < ds.Count; ++ix)
            {
                ds.Y[ix] = System.Math.Sqrt(System.Math.Pow(real[ix], 2.0) + System.Math.Pow(imag[ix], 2.0));
            }
            
            return ds;
        }
        
        public FftResult FftShift()
        {
            double[] xi = new double[1024];
            
            double delta = (this.X[this.Count-1] - this.X[0]) / (xi.Length - 1);
            
            for (int ix=0; ix < xi.Length; ++ix)
            {
                xi[ix] = this.X[0] + (double)ix * delta;
            }
            
            DataSet ds = this.Interpolate(xi);
            double[] real = ds.Y.ToArray();
            double[] imag = new double[real.Length];

            Fourier.Forward(real, imag);

            return new FftResult(real, imag, delta);
        }
        
        public DataSet AutoScale(AutoScaleParameter parameter)
        {
            List<double> y = new List<double>(this.Count);
            
            List<double> means  = parameter.Means.Interpolate(this.X).Y;
            List<double> sigmas = parameter.Sigmas.Interpolate(this.X).Y;
            
            for (int ix=0; ix < this.Count; ++ix)
            {
                y.Add((this.Y[ix] - means[ix]) / sigmas[ix]);
            }
            
            return new DataSet(this.X, y);
        }

        public DataSet MSC(MscParameter parameter)
        {
            double[] p = LevenbergMarquardt.Fit(new LevenbergMarquardt.MscFit(this, parameter.Means, 1.0)).Coefficients;
            return (this  - p[0]) / p[1];
        }
        
        public DataSet Detrend()
        {
            double[] p = LevenbergMarquardt.Fit(new LevenbergMarquardt.QuadraticFit(this, 1.0)).Coefficients;
            List<double> y = new List<double>(this.X.Count);
            foreach (double x in this.X)
            {
                y.Add(p[0] + p[1]* x + p[2]* System.Math.Pow(x, 2.0));
            }
            return this - new DataSet(this.X, y);
        }
        public DataSet Reverse()
        {
            return new DataSet(this.X.AsEnumerable().Reverse(), this.Y.AsEnumerable().Reverse());
        }
    }
}
