// Created with MonoDevelop
//
//    Copyright (C) 2009 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.IO;
using System.Drawing;
using System.Runtime.Serialization;
using System.Globalization;
using System.Linq;

using Hiperscan.Common;
using Hiperscan.Unix;


namespace Hiperscan.Spectroscopy
{

    public enum SpectrumType : int
    {
        Unknown = 0,
        Single = 1,
        Stream = 2,
        QuickStream = 3,
        Preview = 4,
        InternalWhite = 5,
        InternalDark = 6,
        InternalOxid = 7,
        ExternalWhite = 8,
        ExternalEmpty = 9,
        SolidSpecimen = 10,
        FluidSpecimen = 11,
        ProcessedSolidSpecimen = 12,
        ProcessedFluidSpecimen = 13,
        ExternalFluidWhite = 14,
        ReferenceSet = 15,
        CalibrationReference = 16,
        BandwidthReference = 17,
        ProcessedIntensitySpecimen = 18,
        ExternalWhiteInsert = 19
    }

    public enum ProbeType : int
    {
        Unknown = 0,
        FinderStandard = 1,
        FinderInsert = 2,
        FinderSDStandard = 3,
        FinderSDInsert = 4,
        FinderSDSampleSpinner = 5
    }

    public enum LineStyle : int
    {
        Solid,
        Dash,
        Dot,
        DashDot,
        DashDotDot
    }

    public enum MarkerStyle : int
    {
        Cross1,
        Square,
        Circle,
        Diamond
    }

    [Flags]
    public enum CorrectionMask : int
    {
        None = 0,
        Adc = 1,
        WavelengthCorrection = 2,
        SystemFunction = 4,
        DarkIntensity = 8,
        LinearDistortion = 16,
        WavelengthLimits = 32,
        All = 1 | 2 | 8 | 16 | 32 // TODO: SystemFunction correction is disabled for now!
    }

    [Serializable()]
    public class Spectrum : System.Runtime.Serialization.ISerializable, System.IComparable<Spectrum>
    {
        public static readonly bool __SerializationCompatibilityMode = (Environment.GetEnvironmentVariable(Env.HCL.SERIALIZATION_LEGACY_MODE) == "1");

        private CultureInfo culture_info;

        private DataSet raw_intensity;
        private DataSet __intensity;
        private DataSet absorbance;
        private DataSet reflectance;
        private Spectrum background;
        private uint[] add_data;
        private double? absorbance_limit;

        private DarkIntensity dark_intensity;
        private WavelengthCorrection wavelength_correction;
        private WavelengthLimits wavelength_limits;
        private DataSet adc_correction;
        private double[] system_function_correction = null;
        private LinearDistortion linear_distortion;
        private SpectrumType spectrum_type;
        private ProbeType probe_type;

        private string id;
        private string plot_id;
        private string label;
        private string creator;
        private bool is_visible;
        private DateTime date_time;
        private FileInfo file_info;

        private string serial;
        private string firmware_version;
        private string firmware_revision;
        private string lut_timestamp;
        private string comment;
        private int average_count;
        private double spectral_resolution;

        private byte[] hardware_info;

        private long number;
        private volatile bool is_complete;
        private volatile bool from_file;
        private volatile bool limit_bandwidth;

        private volatile bool default_label;
        private volatile bool default_style = true;

        private static long instances = 0;

        private Color color = Color.Empty;
        private float line_width = 2f;
        private LineStyle line_style = LineStyle.Solid;
        private MarkerStyle marker_style = MarkerStyle.Cross1;
        private int z_order = int.MaxValue;


        public Spectrum(WavelengthLimits limits)
        {
            this.culture_info = CultureInfo.CurrentCulture;

            this.id = "spectrum_" + Spectrum.instances.ToString();
            this.label = string.Empty;
            this.comment = string.Empty;
            this.serial = string.Empty;
            this.firmware_version = string.Empty;
            this.firmware_revision = string.Empty;
            this.lut_timestamp = string.Empty;
            this.creator = string.Empty;
            this.is_visible = true;
            this.date_time = DateTime.Now;

            this.hardware_info = new byte[0];

            this.dark_intensity = new DarkIntensity(0.0, 0.0);
            this.spectrum_type = SpectrumType.Unknown;
            this.probe_type = ProbeType.Unknown;
            this.__intensity = null;
            this.absorbance = null;
            this.absorbance_limit = null;
            this.reflectance = null;
            this.add_data = null;
            this.number = -1;
            this.file_info = null;
            this.is_complete = true;
            this.from_file = true;
            this.average_count = -1;
            this.spectral_resolution = 10.0;

            this.wavelength_correction = null;
            this.adc_correction = null;
            this.linear_distortion = null;
            this.limit_bandwidth = false;

            if (limits != null)
                this.wavelength_limits = new WavelengthLimits(limits);
            else
                this.wavelength_limits = null;

            ++Spectrum.instances;
        }

        public Spectrum(DataSet raw_intensity, WavelengthLimits limits) : this(limits)
        {
            this.raw_intensity = new DataSet(raw_intensity);
        }

        public Spectrum(List<double> x, double y, WavelengthLimits limits) : this(limits)
        {
            List<double> ylist = new List<double>();
            for (int ix = 0; ix < x.Count; ++ix)
            {
                ylist.Add(y);
            }
            this.raw_intensity = new DataSet(x, ylist);
        }

        public Spectrum(Spectrum spectrum) : this(spectrum.RawIntensity, spectrum.WavelengthLimits)
        {
            this.culture_info = new CultureInfo(spectrum.CultureInfo.Name);
            this.date_time = spectrum.Timestamp;

            if (spectrum.DarkIntensity != null)
                this.dark_intensity = new DarkIntensity(spectrum.DarkIntensity);

            if (spectrum.HasBackground)
                this.background = spectrum.Background.Clone();

            this.creator = spectrum.Creator;
            this.spectrum_type = spectrum.SpectrumType;
            this.probe_type = spectrum.ProbeType;
            this.serial = spectrum.Serial;
            this.firmware_version = spectrum.FirmwareVersion;
            this.firmware_revision = spectrum.FirmwareRevision;
            this.average_count = spectrum.AverageCount;
            this.lut_timestamp = spectrum.LutTimestamp;
            this.comment = spectrum.Comment;

            if (spectrum.HasAddData)
                this.add_data = (uint[])spectrum.AddData.Clone();

            this.spectral_resolution = spectrum.SpectralResolution;

            this.number      = spectrum.Number;
            this.file_info   = spectrum.FileInfo;
            this.is_complete = spectrum.IsComplete;
            this.from_file   = spectrum.IsFromFile;

            if (spectrum.HasWavelengthCorrection)
                this.wavelength_correction = new WavelengthCorrection(spectrum.WavelengthCorrection);

            if (spectrum.HasAdcCorrection)
                this.adc_correction = new DataSet(spectrum.AdcCorrection);

            if (spectrum.HasLinearDistortion)
                this.linear_distortion = new LinearDistortion(spectrum.LinearDistortion);

            if (spectrum.HasSystemFunctionCorrection)
                this.system_function_correction = (double[])spectrum.SystemFunctionCorrection.Clone();

            this.limit_bandwidth = spectrum.LimitBandwidth;

            if (spectrum.HardwareInfo != null)
                this.hardware_info = (byte[])spectrum.HardwareInfo.Clone();

            this.UpdateAbsorbance();
        }

        public Spectrum(SerializationInfo info, StreamingContext context) : this(WavelengthLimits.DefaultWavelengthLimits)
        {
            this.culture_info = (CultureInfo)info.GetValue("CultureInfo", typeof(CultureInfo));
            this.raw_intensity = (DataSet)info.GetValue("RawIntensity", typeof(DataSet));
            this.date_time = (DateTime)info.GetValue("Time", typeof(DateTime));
            this.dark_intensity = (DarkIntensity)info.GetValue("DarkIntensity", typeof(DarkIntensity));
            this.background = (Spectrum)info.GetValue("Background", typeof(Spectrum));
            this.spectrum_type = (SpectrumType)info.GetValue("SpectrumType", typeof(SpectrumType));
            this.probe_type = (ProbeType)info.GetValue("ProbeType", typeof(ProbeType));
            this.serial = (string)info.GetValue("Serial", typeof(string));
            this.firmware_version = (string)info.GetValue("FirmwareVersion", typeof(string));
            this.lut_timestamp = (string)info.GetValue("LutTimestamp", typeof(string));
            this.average_count = (int)info.GetValue("AverageCount", typeof(int));
            this.limit_bandwidth = (bool)info.GetValue("LimitBandwidth", typeof(bool));
            this.comment = (string)info.GetValue("Comment", typeof(string));
            this.creator = (string)info.GetValue("Creator", typeof(string));
            this.id = (string)info.GetValue("Id", typeof(string));
            this.label = (string)info.GetValue("Label", typeof(string));
            this.file_info = (FileInfo)info.GetValue("FileInfo", typeof(FileInfo));

            try
            {
                this.wavelength_correction = (WavelengthCorrection)info.GetValue("WavelengthCorrection", typeof(WavelengthCorrection));
            }
            catch (SerializationException)
            {
                this.wavelength_correction = null;
            }

            try
            {
                this.wavelength_limits = (WavelengthLimits)info.GetValue("WavelengthLimits", typeof(WavelengthLimits));
            }
            catch (SerializationException)
            {
                this.wavelength_limits = new WavelengthLimits(1000.0, 1900.0);
            }

            try
            {
                this.adc_correction = (DataSet)info.GetValue("AdcCorrection", typeof(DataSet));
            }
            catch (SerializationException)
            {
                this.adc_correction = null;
            }

            try
            {
                this.system_function_correction = (double[])info.GetValue("SystemFunctionCorrection", typeof(double[]));
            }
            catch (SerializationException)
            {
                this.system_function_correction = null;
            }

            try
            {
                this.linear_distortion = (LinearDistortion)info.GetValue("LinearDistortion", typeof(LinearDistortion));
            }
            catch (SerializationException)
            {
                this.linear_distortion = null;
            }

            try
            {
                this.hardware_info = (byte[])info.GetValue("HardwareVersionInfos", typeof(byte[]));
            }
            catch (SerializationException)
            {
                this.hardware_info = new byte[0];
            }

            try
            {
                this.spectral_resolution = (double)info.GetValue("SpectralResolution", typeof(double));
            }
            catch (SerializationException)
            {
                this.spectral_resolution = 10.0;
            }

            try
            {
                this.firmware_revision = (string)info.GetValue("FirmwareRevision", typeof(string));
            }
            catch (SerializationException)
            {
                this.firmware_revision = string.Empty;
            }

            this.from_file = true;
            this.is_visible = true;
            this.__intensity = null;

            this.UpdateAbsorbance();
        }

        public void GetObjectData(SerializationInfo info, StreamingContext ctxt)
        {
            info.AddValue("CultureInfo", this.culture_info);
            info.AddValue("RawIntensity", this.raw_intensity);
            info.AddValue("Time", this.date_time);
            info.AddValue("DarkIntensity", this.dark_intensity);
            info.AddValue("Background", this.background);

            if (Spectrum.__SerializationCompatibilityMode)
            {
                info.AddValue("Type", this.spectrum_type);
            }
            else
            {
                info.AddValue("SpectrumType", this.spectrum_type);
                info.AddValue("ProbeType", this.probe_type);
                info.AddValue("FirmwareRevision", this.firmware_revision);
            }

            info.AddValue("Serial", this.serial);
            info.AddValue("FirmwareVersion", this.firmware_version);
            info.AddValue("LutTimestamp", this.lut_timestamp);
            info.AddValue("AverageCount", this.average_count);
            info.AddValue("LimitBandwidth", this.limit_bandwidth);
            info.AddValue("Creator", this.creator);
            info.AddValue("Comment", this.comment);
            info.AddValue("Id", this.id);
            info.AddValue("Label", this.label);
            info.AddValue("FileInfo", this.file_info);

            if (this.HasWavelengthCorrection)
                info.AddValue("WavelengthCorrection", this.wavelength_correction);
            if (this.HasWavelengthLimits)
                info.AddValue("WavelengthLimits", this.wavelength_limits);
            if (this.HasAdcCorrection)
                info.AddValue("AdcCorrection", this.adc_correction);
            if (this.HasSystemFunctionCorrection)
                info.AddValue("SystemFunctionCorrection", this.system_function_correction);
            if (this.HasLinearDistortion)
                info.AddValue("LinearDistortion", this.linear_distortion);

            info.AddValue("HardwareVersionInfos", this.hardware_info);
            info.AddValue("SpectralResolution", this.spectral_resolution);
        }

        public int CompareTo(Spectrum spectrum)
        {
            if (spectrum == null)
                return 1;

            return this.Timestamp.CompareTo(spectrum.Timestamp);
        }

        private void UpdateAbsorbance()
        {

            if (this.HasBackground && this.HasDarkIntensity)
            {
                this.absorbance = DataSet.Absorbance(this.Intensity, this.Background.Intensity, this.DarkIntensity);
                this.absorbance_limit = DataSet.AbsorbanceLimit(this.Background.Intensity, this.DarkIntensity);
            }
            else
            {
                this.absorbance = null;
                this.absorbance_limit = null;
            }
        }

        private void UpdateReflectance()
        {
            if (this.HasBackground && this.HasDarkIntensity)
                this.reflectance = DataSet.Reflectance(this.Intensity, this.Background.Intensity);
            else
                this.reflectance = null;
        }

        public Spectrum Clone()
        {
            Spectrum spectrum = new Spectrum(this);
            spectrum.id = this.Id;
            spectrum.label = this.Label;
            spectrum.DefaultLabel = this.DefaultLabel;
            spectrum.SpectrumType = this.SpectrumType;

            if (!this.DefaultStyle)
            {
                spectrum.Color = this.Color;
                spectrum.LineStyle = this.LineStyle;
                spectrum.LineWidth = this.LineWidth;
            }

            --Spectrum.instances;

            return spectrum;
        }

        public Spectrum FirLowpassFilter(double[] coeffs)
        {
            Spectrum spectrum = new Spectrum(this);

            if (!this.HasBackground || !this.HasDarkIntensity)
            {
                spectrum.RawIntensity = this.RawIntensity.FirLowpassFilter(coeffs);
                return spectrum;
            }
            else
            {
                Spectrum bg = this.Background.Clone();
                bg.RawIntensity = bg.RawIntensity.FirLowpassFilter(coeffs);
                spectrum.Background = bg;
                spectrum.RawIntensity = this.RawIntensity.FirLowpassFilter(coeffs);
                return spectrum;
            }
        }

        public void ApplyCorrectionsToRawData(CorrectionMask mask)
        {
            DataSet ds = this.GetCorrectedRawIntensity(mask, false);

            if (this.HasDarkIntensity)
            {
                DarkIntensity di = this.GetCorrectedDarkIntensity(mask);

                if ((mask & CorrectionMask.DarkIntensity) != CorrectionMask.None)
                {
                    if (this.HasAdcCorrection && (mask & CorrectionMask.Adc) == CorrectionMask.None)
                        this.adc_correction = null; // FIXME: should AdcCorrection be shifted instead of deleted?
                                                    //						throw new ArgumentException(Catalog.GetString("Cannot apply dark intensity correction without applying ADC correction."));
                    if (this.HasLinearDistortion && (mask & CorrectionMask.LinearDistortion) == CorrectionMask.None)
                        throw new ArgumentException(Catalog.GetString("Cannot apply dark intensity correction without applying Linear Distortion correction."));
                    ds -= di.Intensity;
                }
                else
                {
                    this.dark_intensity = di;
                }
            }

            if (this.HasWavelengthCorrection &&
                (mask & CorrectionMask.WavelengthCorrection) != CorrectionMask.None)
                ds = this.InterpolateWavelengths(ds, this.RawIntensity.X);

            if (this.HasWavelengthLimits &&
                (mask & CorrectionMask.WavelengthLimits) != CorrectionMask.None)
            {
                List<int> indices = ds.Find('x', '<', this.WavelengthLimits.Minimum);
                ds.RemoveAt(indices);
                indices = ds.Find('x', '>', this.WavelengthLimits.Maximum);
                ds.RemoveAt(indices);
            }

            this.RawIntensity = ds;

            if (this.HasBackground)
                this.Background.ApplyCorrectionsToRawData(mask);

            if (this.HasAdcCorrection &&
                (mask & CorrectionMask.Adc) != CorrectionMask.None)
                this.adc_correction = null;

            if (this.HasSystemFunctionCorrection &&
                (mask & CorrectionMask.SystemFunction) != CorrectionMask.None)
                this.system_function_correction = null;

            if (this.HasWavelengthCorrection &&
                (mask & CorrectionMask.WavelengthCorrection) != CorrectionMask.None)
                this.wavelength_correction = new WavelengthCorrection();

            if (this.HasWavelengthLimits &&
                (mask & CorrectionMask.WavelengthLimits) != CorrectionMask.None)
                this.wavelength_limits = null;

            if (this.HasDarkIntensity &&
                (mask & CorrectionMask.DarkIntensity) != CorrectionMask.None)
            {
                this.dark_intensity = new DarkIntensity(double.Epsilon, this.DarkIntensity.Sigma);
            }

            if (this.HasLinearDistortion &&
                (mask & CorrectionMask.LinearDistortion) != CorrectionMask.None)
                this.linear_distortion = new LinearDistortion(0.0, 1.0);

            this.__intensity = null;
            this.UpdateAbsorbance();
        }

        private DataSet InterpolateWavelengths(DataSet intensity, List<double> wavelengths)
        {
            List<double> x = new List<double>(intensity.X);
            List<double> y = new List<double>(intensity.Y);

            x.Insert(0, wavelengths[0] - 200.0);
            x.Insert(0, wavelengths[0] - 400.0);
            x.Add(wavelengths[wavelengths.Count - 1] + 200.0);
            x.Add(wavelengths[wavelengths.Count - 1] + 400.0);

            y.Insert(0, y[0]);
            y.Insert(0, y[0]);
            y.Add(y[y.Count - 1]);
            y.Add(y[y.Count - 1]);

            DataSet ds = new DataSet(x, y);

            return ds.Interpolate(wavelengths, InterpolationType.NaturalCubic);
        }

        public static LinearDistortion MatchIntensities(Spectrum ref_spectrum, Spectrum new_spectrum, double min, double max)
        {
            List<Spectrum> ref_spectra = new List<Spectrum>();
            List<Spectrum> new_spectra = new List<Spectrum>();

            ref_spectra.Add(ref_spectrum);
            new_spectra.Add(new_spectrum);

            return Spectrum.MatchIntensities(ref_spectra, new_spectra, min, max);
        }

        public static LinearDistortion MatchIntensities(List<Spectrum> ref_spectra, List<Spectrum> fit_spectra, double min, double max)
        {
            if (ref_spectra.Count != fit_spectra.Count)
                throw new ArgumentException(Catalog.GetString("Dimension of reference spectra and new spectra must not be different."));

            DataSet ref_intensity = null;
            DataSet fit_intensity = null;

            for (int ix = 0; ix < ref_spectra.Count; ++ix)
            {
                Spectrum ref_spectrum = new Spectrum(ref_spectra[ix]);
                ref_spectrum.ApplyCorrectionsToRawData(CorrectionMask.All & ~CorrectionMask.DarkIntensity);

                Spectrum fit_spectrum = new Spectrum(fit_spectra[ix]);
                fit_spectrum.ApplyCorrectionsToRawData(CorrectionMask.All & ~CorrectionMask.DarkIntensity);

                DataSet ref_ds = ref_spectrum.RawIntensity;
                DataSet fit_ds = fit_spectrum.RawIntensity.Interpolate(ref_spectrum.RawIntensity.X);

                List<int> indices = ref_ds.Find('X', '<', min);
                ref_ds.RemoveAt(indices);
                fit_ds.RemoveAt(indices);
                indices = ref_ds.Find('X', '>', max);
                ref_ds.RemoveAt(indices);
                fit_ds.RemoveAt(indices);

                if (ref_intensity == null)
                    ref_intensity = ref_ds;
                else
                    ref_intensity.AddRange(ref_ds);

                if (fit_intensity == null)
                    fit_intensity = fit_ds;
                else
                    fit_intensity.AddRange(fit_ds);
            }

            //double[] p = LevenbergMarquardt.Fit(ref_intensity, fit_intensity, LevenbergMarquardt.FitType.MatchLinear, 1.0);
            double[] p = LevenbergMarquardt.Fit(new MatchLinearLMFit(ref_intensity, fit_intensity, 1.0)).Coefficients;

            return new LinearDistortion(p[0], p[1]);
        }

        private DataSet GetCorrectedRawIntensity(CorrectionMask mask, bool apply_filter)
        {
            if (!this.HasAdcCorrection &&
                !this.HasSystemFunctionCorrection &&
                !this.LimitBandwidth &&
                !this.HasWavelengthCorrection &&
                !this.HasWavelengthLimits &&
                !this.HasLinearDistortion)
                return this.RawIntensity;

            DataSet ds = new DataSet(this.RawIntensity);

            if (this.HasAdcCorrection &&
                (mask & CorrectionMask.Adc) != CorrectionMask.None)
                ds -= this.adc_correction.Interpolate(ds.Y, InterpolationType.NaturalCubic);

            if (this.HasSystemFunctionCorrection &&
                (mask & CorrectionMask.SystemFunction) != CorrectionMask.None)
                ds = ds.FirLowpassFilter(this.system_function_correction);

            if (apply_filter)
                ds = ds.FirLowpassFilter(DataSet.Get15nmFirLowpassCoefficients());

            if (this.HasWavelengthCorrection &&
                !this.WavelengthCorrection.IsEmpty &&
                (mask & CorrectionMask.WavelengthCorrection) != CorrectionMask.None)
                ds = this.WavelengthCorrection.CorrectedSpectrum(ds);

            if (this.HasWavelengthLimits &&
                (mask & CorrectionMask.WavelengthLimits) != CorrectionMask.None)
                ds = this.WavelengthLimits.CorrectedSpectrum(ds);

            if (this.HasLinearDistortion &&
                (mask & CorrectionMask.LinearDistortion) != CorrectionMask.None)
            {
                double a = this.LinearDistortion.Offset;
                double b = this.LinearDistortion.Factor;
                if (a != 0.0 || b != 1.0)
                {
                    DataSet.Operand offset = new DataSet.Operand(0.0, a);
                    DataSet.Operand factor = new DataSet.Operand(1.0, b);
                    ds = (ds * factor) + offset;
                }
            }

            return ds;
        }

        public DarkIntensity GetCorrectedDarkIntensity(CorrectionMask mask)
        {
            if ((!this.HasAdcCorrection || (mask & CorrectionMask.Adc) == CorrectionMask.None) &&
                (!this.HasLinearDistortion || (mask & CorrectionMask.LinearDistortion) == CorrectionMask.None))
                return this.DarkIntensity;

            if (!this.HasDarkIntensity)
                return this.DarkIntensity;

            double di = this.DarkIntensity.Intensity;

            if (this.HasAdcCorrection && (mask & CorrectionMask.Adc) != CorrectionMask.None)
            {
                DataSet correction = this.adc_correction.Interpolate(new double[] { di }, InterpolationType.NaturalCubic);
                di -= correction.Y[0];
            }

            double a = 0.0;
            double b = 1.0;
            if (this.HasLinearDistortion && (mask & CorrectionMask.LinearDistortion) != CorrectionMask.None)
            {
                a = this.LinearDistortion.Offset;
                b = this.LinearDistortion.Factor;
            }

            return new DarkIntensity(di * b + a, this.DarkIntensity.Sigma);
        }

        private string _na(double val, string fmt, string unit)
        {
            if (val <= 0.0)
                return Catalog.GetString("n/a");
            return val.ToString(fmt) + unit;
        }

        private string _na(string val)
        {
            if (string.IsNullOrEmpty(val))
                return Catalog.GetString("n/a");
            return val;
        }

        public override int GetHashCode()
        {
            int hash = this.Timestamp.GetHashCode();
            hash *= 397;
            hash ^= this.Serial.GetHashCode();
            hash *= 397;
            hash ^= this.HasAbsorbance.GetHashCode();

            return hash;
        }

        public override bool Equals(object obj)
        {
            Spectrum spectrum = obj as Spectrum;

            if (spectrum == null)
                return false;

            if (this.Timestamp != spectrum.Timestamp)
                return false;

            if (this.Serial != spectrum.Serial)
                return false;

            if (this.HasAbsorbance != spectrum.HasAbsorbance)
                return false;

            if (this.HasAbsorbance)
                return this.Absorbance.Equals(spectrum.Absorbance);
            else
                return this.Intensity.Equals(spectrum.Intensity);
        }

        public static Spectrum GetMeanAbsorbance(IEnumerable<Spectrum> spectra)
        {
            if (spectra == null || spectra.Count() < 1)
                throw new Exception("At least one spectrum is needed to calculate a mean spectrum.");

            return Spectrum.GetMeanAbsorbance(spectra.Select(s => s.Absorbance), spectra.First().WavelengthLimits, spectra.First().CultureInfo);
        }

        public static Spectrum GetMeanAbsorbance(IEnumerable<DataSet> data_sets, WavelengthLimits wavelength_limits, CultureInfo culture)
        {
            DataSet mean_ds   = DataSet.Mean(data_sets);
            DataSet reference = new DataSet(mean_ds.X, 1.0);
            DataSet intensity = reference / DataSet.Pow(10.0, mean_ds);

            Spectrum spectrum      = new Spectrum(intensity, wavelength_limits);
            spectrum.Background    = new Spectrum(reference, wavelength_limits);
            spectrum.DarkIntensity = new DarkIntensity(double.Epsilon, 0.0);
            spectrum.CultureInfo   = culture;

            return spectrum;
        }

        public override string ToString()
        {
            //			if (this.FileInfo != null)
            //				return string.Format("spectrum: {0}, {1}, {2}", this.SpectrumType.ToString(), this.FileInfo.FullName, this.DateTime);
            //			else
            //				return string.Format("spectrum: {0}, {1}", this.SpectrumType.ToString(), this.DateTime);
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            sb.AppendFormat(Catalog.GetString("Type:                      {0}"), this.SpectrumType);
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Label:                     {0}"), _na(this.Label));
            sb.AppendLine();
            string fname = string.Empty;
            string dirname = string.Empty;
            if (this.FileInfo != null)
            {
                fname = this.FileInfo.Name;
                dirname = this.FileInfo.DirectoryName;
            }
            sb.AppendFormat(Catalog.GetString("Filename:                  {0}"), _na(fname));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Location:                  {0}"), _na(dirname));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Timestamp:                 {0}"), _na(this.Timestamp.ToString()));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Dark intensity value:      {0}"), _na(this.DarkIntensity.Intensity, "f5", ""));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Dark intensity sigma:      {0}"), _na(this.DarkIntensity.Sigma, "f5", ""));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Dark intensity timestamp:  {0}"), _na(this.DarkIntensity.DateTime.ToString()));
            sb.AppendLine();
            double min = -1.0;
            double max = -1.0;
            if (this.HasWavelengthLimits)
            {
                min = this.WavelengthLimits.Minimum;
                max = this.WavelengthLimits.Maximum;
            }
            sb.AppendFormat(Catalog.GetString("Wavelength minimum:        {0}"), _na(min, "f1", "nm"));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Wavelength maximum:        {0}"), _na(max, "f1", "nm"));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Spectral resolution:       {0}"), _na(this.SpectralResolution, "f1", "nm"));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Average count:             {0}"), _na(this.AverageCount, "f0", ""));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Spectrum type:             {0}"), _na(this.SpectrumType.ToString()));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Probe type:                {0}"), _na(this.ProbeType.ToString()));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Has ADC correction:        {0}"), _na(this.HasAdcCorrection.ToString()));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Has wavelength correction: {0}"), _na(this.HasAdcCorrection.ToString()));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Spectrometer serial:       {0}"), _na(this.Serial));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Firmware version:          {0}"), _na(this.FirmwareVersion));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Firmware revision:         {0}"), _na(this.FirmwareRevision));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("LUT timestamp:             {0}"), _na(this.LutTimestamp));
            sb.AppendLine();
            string hw_info = string.Empty;
            foreach (byte hwi in this.HardwareInfo)
            {
                hw_info += hwi + this.CultureInfo.TextInfo.ListSeparator;
            }
            sb.AppendFormat(Catalog.GetString("Hardware info:             {0}"), _na(hw_info.TrimEnd(this.CultureInfo.TextInfo.ListSeparator.ToCharArray())));
            sb.AppendLine();
            sb.AppendFormat(Catalog.GetString("Creator:                   {0}"), _na(this.Creator));
            sb.AppendLine();
            for (int ix = 0; this.AddData != null && ix < this.AddData.Length; ++ix)
            {
                sb.AppendFormat(Catalog.GetString("Additional data:           {0}"), _na(this.AddData[ix], "f0", ""));
                if (ix < this.AddData.Length - 1)
                    sb.AppendLine();
            }

            return sb.ToString();
        }

        public bool HasBackground
        {
            get { return this.background != null; }
        }

        public bool HasAbsorbance
        {
            get
            {
                if (this.absorbance == null || this.absorbance_limit == null)
                    this.UpdateAbsorbance();
                return this.absorbance != null && this.absorbance_limit != null;
            }
        }

        public bool HasReflectance
        {
            get
            {
                if (this.reflectance == null)
                    this.UpdateReflectance();
                return this.reflectance != null;
            }
        }

        public bool HasDarkIntensity
        {
            get { return this.DarkIntensity.Intensity != 0.0; }
        }

        public bool HasWavelengthCorrection
        {
            get { return this.wavelength_correction != null; }
        }

        public bool HasWavelengthLimits
        {
            get { return this.wavelength_limits != null; }
        }

        public bool HasAdcCorrection
        {
            get { return this.adc_correction != null; }
        }

        public bool HasSystemFunctionCorrection
        {
            get { return this.system_function_correction != null; }
        }

        public bool HasLinearDistortion
        {
            get { return this.linear_distortion != null; }
        }

        public bool HasAddData
        {
            get { return this.add_data != null && this.add_data.Length > 0; }
        }

        public DataSet RawIntensity
        {
            get { return this.raw_intensity; }
            set
            {
                this.raw_intensity = value;
                this.raw_intensity.Id = this.Id;
                this.raw_intensity.Label = this.Label;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public Spectrum Background
        {
            get { return this.background; }
            set
            {
                if (value != null)
                {
                    this.background = new Spectrum(value);
                    this.background.Id = this.Id;
                    this.background.Label = this.Label;
                    if (this.HasDarkIntensity && !this.background.HasDarkIntensity)
                        this.background.DarkIntensity = this.DarkIntensity;
                    if (this.HasLinearDistortion && !this.background.HasLinearDistortion)
                        this.background.LinearDistortion = this.LinearDistortion;
                    this.background.LimitBandwidth = this.LimitBandwidth;
                }
                else
                {
                    this.background = null;
                }
                this.absorbance = null;
                this.absorbance_limit = null;
                this.reflectance = null;
            }
        }

        public DataSet Intensity
        {
            get
            {
                if (this.__intensity == null)
                {
                    DataSet ds = this.GetCorrectedRawIntensity(CorrectionMask.All, this.LimitBandwidth);
                    ds.DataType = DataType.Intensity;
                    ds.Label = this.Label;
                    ds.Id = this.Id;

                    if (this.HasDarkIntensity)
                        ds -= this.GetCorrectedDarkIntensity(CorrectionMask.All).Intensity;

                    this.__intensity = ds;
                }
                return this.__intensity;
            }
        }

        public DarkIntensity DarkIntensity
        {
            get { return this.dark_intensity; }
            set
            {
                this.dark_intensity = value;
                if (this.HasBackground)
                    this.Background.DarkIntensity = value;
                this.__intensity = null;
                this.absorbance = null;
                this.absorbance_limit = null;
                this.reflectance = null;
            }
        }

        public WavelengthCorrection WavelengthCorrection
        {
            get { return this.wavelength_correction; }
            set
            {
                this.wavelength_correction = value;
                if (this.HasBackground)
                    this.Background.WavelengthCorrection = value;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public WavelengthLimits WavelengthLimits
        {
            get { return this.wavelength_limits; }
            set
            {
                this.wavelength_limits = value;
                if (this.HasBackground)
                    this.Background.WavelengthLimits = value;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public DataSet AdcCorrection
        {
            get { return this.adc_correction; }
            set
            {
                if (this.adc_correction == value)
                    return;
                this.adc_correction = value;
                if (this.HasBackground)
                    this.Background.AdcCorrection = value;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public double[] SystemFunctionCorrection
        {
            get { return this.system_function_correction; }
            set
            {
                if (this.system_function_correction != null &&
                    this.system_function_correction.GetHashCode() == value.GetHashCode())
                    return;
                this.system_function_correction = value;
                if (this.HasBackground)
                    this.Background.SystemFunctionCorrection = value;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public LinearDistortion LinearDistortion
        {
            get { return this.linear_distortion; }
            set
            {
                if (this.linear_distortion == value)
                    return;
                this.linear_distortion = value;
                if (this.HasBackground)
                    this.Background.LinearDistortion = value;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public DataSet Absorbance
        {
            get
            {
                if (this.absorbance == null)
                    this.UpdateAbsorbance();
                return this.absorbance;
            }
        }

        public double AbsorbanceLimit
        {
            get
            {
                if (this.absorbance_limit == null)
                    this.UpdateAbsorbance();
                return (double)this.absorbance_limit;
            }
        }

        public DataSet Reflectance
        {
            get
            {
                if (this.reflectance == null)
                    this.UpdateReflectance();
                return this.reflectance;
            }
        }

        public void _SetAbsorbance(DataSet ds)
        {
            this.absorbance = new DataSet(ds);
            this.absorbance.Id = this.Id;
            this.absorbance.Label = this.Label;
            this.absorbance.DataType = DataType.Absorbance;
        }

        public string Creator
        {
            get { return this.creator; }
            set { this.creator = value; }
        }

        public string Id
        {
            get { return this.id; }
            set
            {
                this.id = value;
                this.RawIntensity.Id = value;

                if (this.__intensity != null)
                    this.__intensity.Id = value;
                if (this.HasBackground)
                    this.background.Id = value;
                if (this.HasAbsorbance)
                    this.absorbance.Id = value;
                if (this.HasReflectance)
                    this.reflectance.Id = value;
            }
        }

        public string PlotId
        {
            get { return this.plot_id; }
            set { this.plot_id = value; }
        }

        public string Label
        {
            get { return this.label; }
            set
            {
                string l = value.Replace("\n", " ");
                this.label = l;
                this.RawIntensity.Label = l;

                if (this.__intensity != null)
                    this.__intensity.Label = l;
                if (this.HasBackground)
                    this.background.Label = l;
                if (this.HasAbsorbance)
                    this.absorbance.Label = l;

                this.DefaultLabel = false;
            }
        }

        public FileInfo FileInfo
        {
            get { return this.file_info; }
            set { this.file_info = value; }
        }

        public string Serial
        {
            get { return this.serial; }
            set { this.serial = value; }
        }

        public string FirmwareVersion
        {
            get { return this.firmware_version; }
            set { this.firmware_version = value; }
        }

        public string FirmwareRevision
        {
            get { return this.firmware_revision; }
            set { this.firmware_revision = value; }
        }

        public string LutTimestamp
        {
            get { return this.lut_timestamp; }
            set { this.lut_timestamp = value; }
        }

        public int AverageCount
        {
            get { return this.average_count; }
            set { this.average_count = value; }
        }

        public int Count
        {
            get { return this.RawIntensity.Count; }
        }

        public SpectrumType SpectrumType
        {
            get { return this.spectrum_type; }
            set { this.spectrum_type = value; }
        }

        public ProbeType ProbeType
        {
            get { return this.probe_type; }
            set { this.probe_type = value; }
        }

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

        public string Comment
        {
            get { return this.comment; }
            set { this.comment = value; }
        }

        public long Number
        {
            get { return this.number; }
            set { this.number = value; }
        }

        public uint[] AddData
        {
            get { return this.add_data; }
            set { this.add_data = value; }
        }

        public bool IsComplete
        {
            get { return this.is_complete; }
            set { this.is_complete = value; }
        }

        public bool IsFromFile
        {
            get { return this.from_file; }
            set { this.from_file = value; }
        }

        public bool LimitBandwidth
        {
            get { return this.limit_bandwidth; }
            set
            {
                if (value == this.limit_bandwidth)
                    return;
                this.limit_bandwidth = value;
                if (this.HasBackground)
                    this.Background.LimitBandwidth = value;
                this.__intensity = null;
                this.absorbance = null;
                this.reflectance = null;
            }
        }

        public bool IsVisible
        {
            get { return this.is_visible; }
            set { this.is_visible = value; }
        }

        public bool IsPreview
        {
            get { return this.Id == "9_preview"; }
        }

        public bool IsStream
        {
            get { return this.SpectrumType == SpectrumType.Stream || this.SpectrumType == SpectrumType.QuickStream; }
        }

        public Color Color
        {
            get { return this.color; }
            set
            {
                this.color = value;
                this.RawIntensity.Color = value;

                if (this.__intensity != null)
                    this.__intensity.Color = value;
                if (this.HasBackground)
                    this.background.Color = value;
                if (this.HasAbsorbance)
                    this.absorbance.Color = value;
                if (this.HasReflectance)
                    this.reflectance.Color = value;

                this.DefaultStyle = false;
            }
        }

        public float LineWidth
        {
            get { return this.line_width; }
            set
            {
                this.line_width = value;
                this.RawIntensity.LineWidth = value;

                if (this.__intensity != null)
                    this.__intensity.LineWidth = value;
                if (this.HasBackground)
                    this.background.LineWidth = value;
                if (this.HasAbsorbance)
                    this.absorbance.LineWidth = value;
                if (this.HasReflectance)
                    this.reflectance.LineWidth = value;

                this.DefaultStyle = false;
            }
        }

        public LineStyle LineStyle
        {
            get { return this.line_style; }
            set
            {
                this.line_style = value;
                this.RawIntensity.LineStyle = value;

                if (this.__intensity != null)
                    this.__intensity.LineStyle = value;
                if (this.HasBackground)
                    this.background.LineStyle = value;
                if (this.HasAbsorbance)
                    this.absorbance.LineStyle = value;
                if (this.HasReflectance)
                    this.reflectance.LineStyle = value;

                this.DefaultStyle = false;
            }
        }

        public MarkerStyle MarkerStyle
        {
            get { return this.marker_style; }
            set
            {
                this.marker_style = value;
                this.RawIntensity.MarkerStyle = value;

                if (this.__intensity != null)
                    this.__intensity.MarkerStyle = value;
                if (this.HasBackground)
                    this.background.MarkerStyle = value;
                if (this.HasAbsorbance)
                    this.absorbance.MarkerStyle = value;
                if (this.HasReflectance)
                    this.reflectance.MarkerStyle = value;

                this.DefaultStyle = false;
            }
        }

        public int ZOrder
        {
            get { return this.z_order; }
            set
            {
                this.z_order = value;
                this.RawIntensity.ZOrder = value;

                if (this.__intensity != null)
                    this.__intensity.ZOrder = value;
                if (this.HasBackground)
                    this.background.ZOrder = value;
                if (this.HasAbsorbance)
                    this.absorbance.ZOrder = value;
                if (this.HasReflectance)
                    this.reflectance.ZOrder = value;
            }
        }

        public bool DefaultLabel
        {
            get { return this.default_label; }
            set { this.default_label = value; }
        }

        public bool DefaultStyle
        {
            get { return this.default_style; }
            set { this.default_style = value; }
        }

        public CultureInfo CultureInfo
        {
            get { return this.culture_info; }
            set { this.culture_info = value; }
        }

        public byte[] HardwareInfo
        {
            get { return this.hardware_info; }
            set { this.hardware_info = value; }
        }

        public double SpectralResolution
        {
            get { return this.spectral_resolution; }
            set { this.spectral_resolution = value; }
        }
    }
}
