// 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.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using Hiperscan.Spectroscopy.IO;
using Hiperscan.Unix;


namespace Hiperscan.Spectroscopy
{
    [DataContract(Name = "Spectrum", Namespace = "http://www.hiperscan.com/spectroscopy")] [Serializable()]
    [KnownType(typeof(GregorianCalendar))]
    public partial class Spectrum : System.IComparable<Spectrum>, System.Runtime.Serialization.IExtensibleDataObject
    {
        public const string STANDARD_CULTURE = "en-US";

        // default values are defined in Initialize() since DataContracts ignore default constructors
        [DataMember] public CultureInfo CultureInfo   { get; private set; }
        [DataMember] public DateTime Timestamp        { get; set; }
        [DataMember] public SpectrumType SpectrumType { get; set; }
        [DataMember] public ProbeType ProbeType       { get; set; }
        [DataMember] public string Serial             { get; set; }
        [DataMember] public string FirmwareVersion    { get; set; }
        [DataMember] public string FirmwareRevision   { get; set; }
        [DataMember] public string LutTimestamp       { get; set; }
        [DataMember] public int AverageCount          { get; set; }
        [DataMember] public FileInfo FileInfo         { get; set; }
        [DataMember] public string Comment            { get; set; }
        [DataMember] public uint[] AddData            { get; set; }
        [DataMember] public string Creator            { get; set; }
        [DataMember] public byte[] HardwareInfo       { get; set; }
        [DataMember] public double SpectralResolution { get; set; }

        public bool DefaultLabel { get; set; }
        public bool DefaultStyle { get; set; }
        public long Number       { get; set; }
        public bool IsComplete   { get; set; }
        public bool IsFromFile   { get; set; }
        public string PlotId     { get; set; }
        public bool IsVisible    { get; set; }

        [DataMember(Name = "RawIntensity")]             private DataSet raw_intensity;
        [DataMember(Name = "Reference")]                private Spectrum reference;
        [DataMember(Name = "DarkIntensity")]            private DarkIntensity dark_intensity;
        [DataMember(Name = "WavelengthCorrection")]     private WavelengthCorrection wavelength_correction;
        [DataMember(Name = "WavelengthLimits")]         private WavelengthLimits wavelength_limits;
        [DataMember(Name = "AdcCorrction")]             private DataSet adc_correction;
        [DataMember(Name = "SystemFunctionCorrection")] private double[] system_function_correction = null;
        [DataMember(Name = "LinearDistortion")]         private LinearDistortion linear_distortion;
        [DataMember(Name = "Id")]                       private string id;
        [DataMember(Name = "Label")]                    private string label;
        [DataMember(Name = "LimitBandwith")]            private volatile bool limit_bandwidth;

        [NonSerialized] private ExtensionDataObject extension_data;
        [NonSerialized] private DataSet _intensity;
        [NonSerialized] private DataSet absorbance;
        [NonSerialized] private DataSet reflectance;
        [NonSerialized] private double? absorbance_limit;

        [NonSerialized] private static long instances = 0;

        [NonSerialized] private Color color;
        [NonSerialized] private float line_width;
        [NonSerialized] private LineStyle line_style;
        [NonSerialized] private MarkerStyle marker_style;
        [NonSerialized] private int z_order;

        public Spectrum()
        {
            this.Initialize();
            this.RawIntensity = new DataSet();
        }

        public Spectrum(WavelengthLimits limits) : this()
        {
            if (limits != null)
                this.WavelengthLimits = new WavelengthLimits(limits);
        }

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

        public Spectrum(IEnumerable<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.CultureInfo = new CultureInfo(spectrum.CultureInfo.Name);
            this.Timestamp = spectrum.Timestamp;

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

            if (spectrum.HasReference)
                this.reference = spectrum.Reference.Clone();

            this.Creator = spectrum.Creator;
            this.SpectrumType = spectrum.SpectrumType;
            this.ProbeType = spectrum.ProbeType;
            this.Serial = spectrum.Serial;
            this.FirmwareVersion = spectrum.FirmwareVersion;
            this.FirmwareRevision = spectrum.FirmwareRevision;
            this.AverageCount = spectrum.AverageCount;
            this.LutTimestamp = spectrum.LutTimestamp;
            this.Comment = spectrum.Comment;

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

            this.SpectralResolution = spectrum.SpectralResolution;

            this.Number = spectrum.Number;
            this.FileInfo = spectrum.FileInfo;
            this.IsComplete = spectrum.IsComplete;
            this.IsFromFile = spectrum.IsFromFile;

            this.WlcMasterSpectrumId = spectrum.WlcMasterSpectrumId;

            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.HardwareInfo = (byte[])spectrum.HardwareInfo.Clone();

            this.UpdateAbsorbance();
        }

        [OnDeserializing]
        private void SetValuesOnDeserializing(StreamingContext context)
        {
            this.Initialize();
        }

        private void Initialize()
        {
            this.IsComplete = true;
            this.IsFromFile = true;

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

            this.CultureInfo = new CultureInfo(Spectrum.STANDARD_CULTURE);

            this.id = "spectrum_" + Spectrum.instances.ToString();
            this.label = string.Empty;
            this.Comment = string.Empty;
            this.Serial = string.Empty;
            this.FirmwareVersion = string.Empty;
            this.FirmwareRevision = string.Empty;
            this.LutTimestamp = string.Empty;
            this.Creator = string.Empty;
            this.IsVisible = true;
            this.Timestamp = DateTime.Now;

            this.HardwareInfo = new byte[0];

            this.dark_intensity = new DarkIntensity(0.0, 0.0);
            this.SpectrumType = SpectrumType.Unknown;
            this.ProbeType = ProbeType.Unknown;
            this._intensity = null;
            this.absorbance = null;
            this.absorbance_limit = null;
            this.reflectance = null;
            this.AddData = null;
            this.Number = -1;
            this.FileInfo = null;
            this.IsComplete = true;
            this.IsFromFile = true;
            this.AverageCount = -1;
            this.SpectralResolution = 10.0;

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

            this.DefaultStyle = true;

            ++Spectrum.instances;
        }

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

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

        private void UpdateAbsorbance()
        {

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

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

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

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

            --Spectrum.instances;

            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 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.HasReference)
                this.Reference.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 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)
        {
            if (!(obj is Spectrum spectrum))
                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 override string ToString()
        {
            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.Timestamp.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("Averaging 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("Created with:              {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 void _SetAbsorbance(DataSet ds)
        {
            this.absorbance = new DataSet(ds)
            {
                Id = this.Id,
                Label = this.Label,
                DataType = DataType.Absorbance
            };
        }

        public ExtensionDataObject ExtensionData
        {
            get { return this.extension_data;  }
            set { this.extension_data = value; }
        }
    }
}
