// Created with MonoDevelop
//
//    Copyright (C) 2018 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.Runtime.Serialization;
using System.Globalization;
using System.Text;

using Hiperscan.Unix;


namespace Hiperscan.Spectroscopy
{

    [Serializable()]
    public class WavelengthCorrection : System.Runtime.Serialization.ISerializable
    {

        public enum ReferenceType
        {
            NIST10nm,
            NIST15nm,
            NIST15nmSubset,
            NIST10nmSubset
        }

        public static readonly double DeviationLimit = 15.0;

        private DataSet deviations;
        private DateTime date_time;
        private double[] fit_parameters;

        private static readonly double[] WAVELENGTH_REFERENCES_NIST10NM = new double[] {
            1132.9, 1261.8, 1320.2, 1534.6, 1681.4, 1757.6, 1847.3
        };
        private static readonly double[] WAVELENGTH_REFERENCES_NIST15NM = new double[] {
            1262.7, 1320.7, 1535.5, 1681.3, 1758.0, 1847.4
        };
        private static readonly double[] WAVELENGTH_REFERENCES_NIST15NM_SUBSET = new double[] {
            1262.7, 1535.5, 1681.3, 1847.4
        };
        private static readonly double[] WAVELENGTH_REFERENCES_NIST10NM_SUBSET = new double[] {
            1261.8, 1534.6, 1681.4, 1757.6
        };

        public WavelengthCorrection()
        {
            this.deviations = new DataSet(WAVELENGTH_REFERENCES_NIST10NM, new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 });
            this.date_time = DateTime.Now;
            this.fit_parameters = new double[] { 0.0, 0.0, 0.0 };
        }

        public WavelengthCorrection(double[] wavelengths, double[] deviations, bool ignore_deviation_limit, LevenbergMarquardt.OptimParams lm_optim_params) : this()
        {
            //          // FIXME: delete first deviation if it's too big
            //          if (Math.Abs(deviations[0] - deviations[1]) > 2.5f)
            //          {
            //              double[] new_wavelengths = new double[wavelengths.Length-1];
            //              double[] new_deviations  = new double[deviations.Length-1];
            //              Array.Copy(wavelengths, 1, new_wavelengths, 0, new_wavelengths.Length);
            //              Array.Copy(deviations,  1, new_deviations,  0, new_deviations.Length);
            //              wavelengths = new_wavelengths;
            //              deviations  = new_deviations;
            //          }

            bool zero_correction = true;
            foreach (double val in deviations)
            {
                if (val != 0.0)
                    zero_correction = false;
            }

            if (!zero_correction)
            {
                this.deviations = new DataSet(wavelengths, deviations);
                //this.fit_parameters = LevenbergMarquardt.Fit(this.deviations, LevenbergMarquardt.FitType.Quadradic, 1.0);
                this.fit_parameters = LevenbergMarquardt.Fit(new QuadraticLMFit(this.deviations, 1.0), lm_optim_params).Coefficients;
            }

            if (!ignore_deviation_limit)
                this.CheckDeviations();
        }

        public WavelengthCorrection(double[] wavelengths, double[] deviations, LevenbergMarquardt.OptimParams lm_optim_params)
            : this(wavelengths, deviations, false, lm_optim_params)
        {
        }

        public WavelengthCorrection(double[] wavelengths, double[] deviations)
            : this(wavelengths, deviations, false, new LevenbergMarquardt.OptimParams())
        {
        }

        public WavelengthCorrection(List<double> wavelengths, List<double> deviations, LevenbergMarquardt.OptimParams lm_optim_params)
            : this(wavelengths.ToArray(), deviations.ToArray(), lm_optim_params)
        {
        }

        public WavelengthCorrection(List<double> wavelengths, List<double> deviations)
            : this(wavelengths.ToArray(), deviations.ToArray(), new LevenbergMarquardt.OptimParams())
        {
        }

        public WavelengthCorrection(WavelengthCorrection wlc)
        {
            this.deviations = new DataSet(wlc.deviations);
            this.date_time = wlc.date_time;
            this.fit_parameters = (double[])wlc.fit_parameters.Clone();
        }

        public WavelengthCorrection(double[] p)
        {
            this.fit_parameters = p;
            this.date_time = DateTime.Now;
            this.deviations = WavelengthCorrection.QuadraticDeviation(p, WAVELENGTH_REFERENCES_NIST10NM);
            this.CheckDeviations();
        }

        public WavelengthCorrection(Spectrum ref_spectrum, Spectrum spectrum, List<double> wavelengths, bool linear_y_fit)
            : this(ref_spectrum, spectrum, wavelengths, linear_y_fit, new LevenbergMarquardt.OptimParams())
        {
        }

        public WavelengthCorrection(Spectrum ref_spectrum, Spectrum spectrum, List<double> wavelengths, bool linear_y_fit, LevenbergMarquardt.OptimParams optim_params)
        {
            Spectrum fit_spectrum = new Spectrum(spectrum);
            fit_spectrum.LimitBandwidth = ref_spectrum.LimitBandwidth;
            fit_spectrum.WavelengthCorrection = null;

            if (wavelengths != null)
                this.__WavelengthCorrectionHelper(ref_spectrum.Absorbance.Interpolate(wavelengths), fit_spectrum.Absorbance, linear_y_fit, optim_params);
            else
                this.__WavelengthCorrectionHelper(ref_spectrum.Absorbance, fit_spectrum.Absorbance, linear_y_fit, optim_params);

            this.CheckDeviations();
        }

        public WavelengthCorrection(DataSet ref_spectrum, DataSet fit_spectrum, bool linear_y_fit, LevenbergMarquardt.OptimParams optim_params)
        {
            this.__WavelengthCorrectionHelper(ref_spectrum, fit_spectrum, linear_y_fit, optim_params);
            this.CheckDeviations();
        }

        private void __WavelengthCorrectionHelper(DataSet ref_spectrum, DataSet fit_spectrum, bool linear_y_fit, LevenbergMarquardt.OptimParams optim_params)
        {
            DataSet ref_ds = ref_spectrum.SNV();
            DataSet fit_ds = fit_spectrum.Interpolate(ref_ds.X).SNV();

            // slope compensation
            fit_ds = fit_ds - fit_ds.LinearRegressionCurve(fit_ds.LinearRegression());
            ref_ds = ref_ds - ref_ds.LinearRegressionCurve(ref_ds.LinearRegression());

            double[] p;
            //if (linear_y_fit)
            //  p = LevenbergMarquardt.Fit(fit_ds, ref_ds, LevenbergMarquardt.FitType.Match2DQuadraticLinearSlope, 1.0);
            //else
            //  p = LevenbergMarquardt.Fit(fit_ds, ref_ds, LevenbergMarquardt.FitType.Match2DQuadraticQuadraticSlope, 1.0);

            if (linear_y_fit)
                p = LevenbergMarquardt.Fit(new Match2DQuadraticLinearSlopeLMFit(fit_ds, ref_ds, 1.0), optim_params).Coefficients;
            else
                p = LevenbergMarquardt.Fit(new Match2DQuadraticQuadraticSlopeLMFit(fit_ds, ref_ds, 1.0), optim_params).Coefficients;

            this.fit_parameters = new double[3];
            Array.Copy(p, 0, this.fit_parameters, 0, 3);
            this.date_time = DateTime.Now;
            this.deviations = WavelengthCorrection.QuadraticDeviation(p, WAVELENGTH_REFERENCES_NIST10NM);
        }

        public WavelengthCorrection(SerializationInfo info, StreamingContext context)
        {
            this.deviations = (DataSet)info.GetValue("Deviations", typeof(DataSet));
            this.date_time = (DateTime)info.GetValue("Time", typeof(DateTime));
            this.fit_parameters = (double[])info.GetValue("FitParameters", typeof(double[]));
        }

        public void GetObjectData(SerializationInfo info, StreamingContext ctxt)
        {
            info.AddValue("Deviations", this.deviations);
            info.AddValue("Time", this.date_time);
            info.AddValue("FitParameters", this.fit_parameters);
        }

        public static DataSet GetWavelengthReferences(ReferenceType ref_type)
        {
            switch (ref_type)
            {

            case ReferenceType.NIST10nm:
                return new DataSet(WAVELENGTH_REFERENCES_NIST10NM, 0.0);

            case ReferenceType.NIST15nm:
                return new DataSet(WAVELENGTH_REFERENCES_NIST15NM, 0.0);

            case ReferenceType.NIST15nmSubset:
                return new DataSet(WAVELENGTH_REFERENCES_NIST15NM_SUBSET, 0.0);

            case ReferenceType.NIST10nmSubset:
                return new DataSet(WAVELENGTH_REFERENCES_NIST10NM_SUBSET, 0.0);

            }

            throw new ArgumentException("Unknown reference type.");
        }

        public static WavelengthCorrection CreateFromExtremumDeviation(Spectrum oxid_spectrum, ReferenceType ref_type)
        {
            return WavelengthCorrection.CreateFromExtremumDeviation(oxid_spectrum, ref_type, false);
        }

        public static WavelengthCorrection CreateFromExtremumDeviation(Spectrum oxid_spectrum, ReferenceType ref_type, bool nist_10nm_optimized)
        {
            DataSet ref_extrema;
            Spectrum spectrum = oxid_spectrum.Clone();
            spectrum.WavelengthCorrection = null;
            ref_extrema = GetWavelengthReferences(ref_type);

            switch (ref_type)
            {

            case ReferenceType.NIST10nm:
            case ReferenceType.NIST10nmSubset:
                spectrum.LimitBandwidth = false;
                break;

            case ReferenceType.NIST15nm:
            case ReferenceType.NIST15nmSubset:
                if (spectrum.SpectralResolution != 15.0)
                    spectrum.LimitBandwidth = true;
                break;

            default:
                throw new NotImplementedException("Unknown reference type.");

            }

            DataSet.Extrema extrema;
            if (nist_10nm_optimized)
                extrema = WavelengthCorrection.FindNISTExtrema10nmResolution(spectrum, ref_extrema);
            else
                extrema = spectrum.Absorbance.FindExtremaSG(5, 15, 0.0, -2.0);

            List<double> x_max = new List<double>();
            List<double> y_max = new List<double>();
            for (int jx = 0; jx < ref_extrema.Count; ++jx)
            {
                double[] maxx = extrema.Maxima.FindNearest(ref_extrema.X[jx]);
                x_max.Add(maxx[0]);
                y_max.Add(maxx[1]);
            }

            List<double> x = new List<double>();
            List<double> y = new List<double>();
            int kx = 0;
            while (kx < ref_extrema.Count &&
               kx < x_max.Count)
            {
                x.Add(ref_extrema.X[kx]);
                y.Add(x_max[kx] - ref_extrema.X[kx]);
                ++kx;
            }

            return new WavelengthCorrection(x, y, new LevenbergMarquardt.OptimParams());
        }

        public DataSet CorrectedSpectrum(DataSet ds)
        {
            double[] corrected = new double[ds.Count];
            double[] p = this.fit_parameters;

            for (int ix = 0; ix < ds.Count; ++ix)
            {
                //double offset = p[0] + p[1] * Math.Pow(ds.X[ix] - p[2], 2f);
                double offset = p[0] + p[1] * ds.X[ix] + p[2] * Math.Pow(ds.X[ix], 2f);
                corrected[ix] = ds.X[ix] - offset;
            }

            return new DataSet(corrected, ds.Y.ToArray());
        }

        public DataSet FitFunction(double start, double end, double increment)
        {
            List<double> X = new List<double>();
            List<double> Y = new List<double>();
            double[] p = this.fit_parameters;

            for (double x = start; x <= end; x += increment)
            {
                X.Add(x);
                Y.Add(WavelengthCorrection.QuadradicFit(p, x));
            }
            return new DataSet(X, Y);
        }

        private static double QuadradicFit(double[] p, double x)
        {
            return p[0] + p[1] * x + p[2] * Math.Pow(x, 2f);
        }

        private static DataSet QuadraticDeviation(double[] p, double[] x)
        {
            double[] y = new double[x.Length];
            for (int ix = 0; ix < x.Length; ++ix)
            {
                y[ix] = WavelengthCorrection.QuadradicFit(p, x[ix]);
            }
            return new DataSet(x, y);
        }

        public static WavelengthCorrection Calculate(DataSet reference_extrema, DataSet current)
        {
            return WavelengthCorrection.Calculate(reference_extrema, current, new LevenbergMarquardt.OptimParams());
        }

        public static WavelengthCorrection Calculate(DataSet reference_extrema, DataSet current, LevenbergMarquardt.OptimParams optim_params)
        {
            int epsilon = 5;
            int window_size = 15;
            double noise_limit = 0.0;
            double cutoff = -2.0;

            DataSet.Extrema extrema = current.FindExtremaSG(epsilon, window_size, noise_limit, cutoff);

            List<double> x_max = new List<double>();
            List<double> y_max = new List<double>();
            int ix;
            for (ix = 0; ix < reference_extrema.Count; ++ix)
            {
                double[] max = extrema.Maxima.FindNearest(reference_extrema.X[ix]);
                x_max.Add(max[0]);
                y_max.Add(max[1]);
            }

            DataSet current_extrema = new DataSet(x_max, y_max);

            List<double> x = new List<double>();
            List<double> y = new List<double>();

            ix = 0;
            while (ix < reference_extrema.Count &&
               ix < current_extrema.Count)
            {
                x.Add(reference_extrema.X[ix]);
                y.Add(current_extrema.X[ix] - reference_extrema.X[ix]);
                ++ix;
            }

            return new WavelengthCorrection(x, y, optim_params);
        }

        public override string ToString()
        {
            return this.ToString(CultureInfo.CurrentCulture);
        }

        public string ToString(CultureInfo culture)
        {
            StringBuilder sb = new StringBuilder();

            for (int ix = 0; ix < this.Wavelengths.Length; ++ix)
            {
                sb.Append(this.Wavelengths[ix].ToString(culture));
                sb.Append(culture.TextInfo.ListSeparator);
                sb.Append(this.Corrections[ix].ToString(culture));
                if (ix < this.Wavelengths.Length - 1)
                    sb.Append(culture.TextInfo.ListSeparator);
            }

            return sb.ToString();
        }

        public override int GetHashCode()
        {
            if (this.fit_parameters == null || this.fit_parameters.Length == 0)
                return base.GetHashCode();

            int hash = 0;
            foreach (double p in this.fit_parameters)
            {
                hash ^= p.GetHashCode();
            }

            return hash;
        }

        public override bool Equals(object o)
        {
            if (o == null)
                return false;

            if (!(o is WavelengthCorrection))
                return false;

            return (this == (o as WavelengthCorrection));
        }

        public static bool operator ==(WavelengthCorrection lhs, WavelengthCorrection rhs)
        {
            if (object.Equals(lhs, null) || object.Equals(rhs, null))
                return (object)lhs == (object)rhs;

            if (lhs.fit_parameters.Length != rhs.fit_parameters.Length)
                return false;

            for (int ix = 0; ix < lhs.fit_parameters.Length; ++ix)
            {
                if (lhs.fit_parameters[ix] != rhs.fit_parameters[ix])
                    return false;
            }

            return true;
        }

        static DataSet.Extrema FindNISTExtrema10nmResolution(Spectrum spectrum, DataSet ref_extrema)
        {
            DataSet minima = new DataSet();
            DataSet maxima = new DataSet();

            DataSet absorbance = spectrum.Absorbance.FirLowpassFilter(DataSet.Get6nmFirLowpassCoefficients());

            foreach (double ref_wavelength in ref_extrema.X)
            {
                //DataSet range = spectrum.Absorbance.GetRange(ref_wavelength - 50.0, ref_wavelength + 50.0);
                DataSet range = absorbance.GetRange(ref_wavelength - 50.0, ref_wavelength + 50.0);
                int window_size = WavelengthCorrection.GetSgWindowSize10nmResolution(ref_wavelength);
                DataSet.Extrema extrema = range.FindExtremaSG(5, window_size, 0.0, -2.0);
                minima.AddRange(extrema.Minima);
                maxima.AddRange(extrema.Maxima);
            }

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

        public static int GetSgWindowSize10nmResolution(double ref_wavelength)
        {
            double eps = 5.0;
            if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[0]) < eps)
                return 21;
            else if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[1]) < eps)
                return 15;
            else if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[2]) < eps)
                return 11;
            else if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[3]) < eps)
                return 13;
            else if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[4]) < eps)
                return 15;
            else if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[5]) < eps)
                return 13;
            else if (Math.Abs(ref_wavelength - WAVELENGTH_REFERENCES_NIST10NM[6]) < eps)
                return 19;
            else
                return 15;
        }

        public static bool operator !=(WavelengthCorrection lhs, WavelengthCorrection rhs)
        {
            return !(lhs == rhs);
        }

        public double[] Wavelengths
        {
            get { return this.deviations.X.ToArray(); }
        }

        public double[] Corrections
        {
            get { return this.deviations.Y.ToArray(); }
        }

        public double[] FitParameters
        {
            get { return this.fit_parameters; }
        }

        public DateTime DateTime
        {
            get { return this.date_time; }
            set { this.date_time = value; }
        }

        public bool IsEmpty
        {
            get
            {
                foreach (double d in this.deviations.Y)
                {
                    if (d != 0f) return false;
                }
                return true;
            }
        }

        private void CheckDeviations()
        {
            foreach (double d in this.deviations.Y)
            {
                if (d > WavelengthCorrection.DeviationLimit)
                    throw new Exception(Catalog.GetString("Invalid wavelength correction (limit exceeded)."));
            }
        }
    }
}