// Created with MonoDevelop
//
//    Copyright (C) 2024 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.Reflection;
using System.Collections.Generic;

using Hiperscan.Unix;
using Hiperscan.Spectroscopy;
using Hiperscan.Spectroscopy.IO;
using Hiperscan.Spectroscopy.Processing.Transformations;

namespace Hiperscan.Spectroscopy.Processing
{
    public abstract partial class SpectrumProcessor
    {
        private static Dictionary<ReferenceWheelVersion,Spectrum> _oxid_references = new Dictionary<ReferenceWheelVersion, Spectrum>();
        private static List<double> zenith_recalibration_wavelengths = null;
        private static object __lock_ref = new object();

        protected virtual void ResetExternalReferences()
        {
            this.processing_black = null;
            this.processing_white = null;
            this.processing_transflectance_insert = null;
            this.processing_sample_insert_white = null;

            this.corrected_oxide_of_sample = null;
            this.corrected_oxide_of_reference = null;

            this.recalc_black_reference = true;
            this.recalc_white_reference = true;
            this.recalc_sample_insert_white_reference = true;
            this.recalc_transflectance_insert_reference = true;

            this.processing_external_dark = null;
            this.processing_external_background = null;
        }

        protected virtual void ResetAll()
        {
            this.ResetExternalReferences();

            this.corrected_oxide_of_sample = null;
            this.corrected_oxide_of_reference = null;
            this.wavelength_correction_of_sample = null;
            this.wavelength_correction_of_references = null;
            this.sample_wavelength_correction_id = Spectrum.UNKNOWN_WLC_MASTER_SPECTRUM_ID;
            this.reference_wavelength_correction_id = Spectrum.UNKNOWN_WLC_MASTER_SPECTRUM_ID;
        }

        public static Spectrum GetWavelengthReference(ReferenceWheelVersion wheel_version)
        {
            string reference_name;

            switch (wheel_version)
            {

                case ReferenceWheelVersion.TitaniumDioxide:
                    reference_name = "wavelength_reference_tio2.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideOEM:
                    reference_name = "wavelength_reference_tio2_oem.csv";
                    break;

                case ReferenceWheelVersion.BariumSulfate:
                    reference_name = "wavelength_reference_baso4.csv";
                    break;

                case ReferenceWheelVersion.ZenithApoIdent1:
                    reference_name = "wavelength_reference_zenith_apo-ident1.csv";
                    break;

                case ReferenceWheelVersion.ZenithApoIdent2:
                    reference_name = "wavelength_reference_zenith_apo-ident2.csv";
                    break;

                case ReferenceWheelVersion.ZenithOEM1:
                    reference_name = "wavelength_reference_zenith_oem1.csv";
                    break;

                case ReferenceWheelVersion.ZenithOEM2:
                    reference_name = "wavelength_reference_zenith_oem2.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideZenithApoIdent:
                case ReferenceWheelVersion.TitaniumDioxideZenithApoIdent2:
                    reference_name = "wavelength_reference_tio2-zenith_apo-ident.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideZenithOEM:
                    reference_name = "wavelength_reference_tio2-zenith_oem.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideZenithOEMSD:
                case ReferenceWheelVersion.TitaniumDioxideZenithOEMSD2:
                    reference_name = "wavelength_reference_tio2-zenith_oemsd.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideZenithFinder2:
                    reference_name = "wavelength_reference_tio2-zenith_finder2.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideZenithFinderX:
                    reference_name = "wavelength_reference_tio2-zenith_finderx.csv";
                    break;

                case ReferenceWheelVersion.TitaniumDioxideZenithApoIdentES20:
                    reference_name = "wavelength_reference_tio2-zenith_apo-ident_es20.csv";
                    break;

                default:
                    throw new NotSupportedException(String.Format(Catalog.GetString("Unknown finder wheel version {0}."), wheel_version));

            }

            Assembly assembly = Assembly.GetExecutingAssembly();
            Spectrum reference_spectrum = CSV.Read(assembly.GetManifestResourceStream("Spectroscopy.Processing.WavelengthReferences." + reference_name));

            return reference_spectrum;
        }

        public static bool HasZenithRecalibrationReference(ReferenceWheelVersion wheel_version)
        {
            return (wheel_version == ReferenceWheelVersion.ZenithApoIdent1 ||
                    wheel_version == ReferenceWheelVersion.ZenithOEM1 ||
                    wheel_version == ReferenceWheelVersion.ZenithApoIdent2 ||
                    wheel_version == ReferenceWheelVersion.ZenithOEM2 || 
                    wheel_version == ReferenceWheelVersion.TitaniumDioxideZenithFinderX);
        }

        protected WavelengthCorrection CalculateWavelengthCorrection(Spectrum oxid_spectrum, bool correction_of_reference, out byte wlc_id)
        {
            wlc_id = Spectrum.UNKNOWN_WLC_MASTER_SPECTRUM_ID;

            if (this.WithWavelengthCorrection == false)
                return new WavelengthCorrection();

            if (this.DontReapplyWavelengthCorrection)
            {
                if (oxid_spectrum.HasWavelengthCorrection)
                { 
                    wlc_id = oxid_spectrum.WlcMasterSpectrumId;
                    return new WavelengthCorrection(oxid_spectrum.WavelengthCorrection);
                }
                else
                    return new WavelengthCorrection();
            }

            if (this.use_default_system_function && oxid_spectrum.HasSystemFunctionCorrection == false)
                oxid_spectrum.SystemFunctionCorrection = DataSet.GetDefaultSystemFunctionCorrectionCoefficients();

            Spectrum oxid = oxid_spectrum.Clone();

            ReferenceWheelVersion wheel_version = this.reference_wheel_version;
            Spectrum master_oxid = null;
            bool is_zenith = false;

            if (this.forced_master_oxid_reference == null)
            { 
                if (wheel_version == ReferenceWheelVersion.Auto)
                    wheel_version = (ReferenceWheelVersion)oxid.HardwareInfo[1];

                is_zenith = SpectrumProcessor.HasZenithRecalibrationReference(wheel_version);

                if (SpectrumProcessor._oxid_references.ContainsKey(wheel_version))
                { 
                    lock (SpectrumProcessor.__lock_ref)
                        master_oxid = SpectrumProcessor._oxid_references[wheel_version];
                }
                else
                {
                    master_oxid = SpectrumProcessor.GetWavelengthReference(wheel_version);
                    lock (SpectrumProcessor.__lock_ref)
                        SpectrumProcessor._oxid_references.Add(wheel_version,master_oxid);
                }

                wlc_id = (byte)wheel_version;
            }
            else
            {
                master_oxid = this.forced_master_oxid_reference;
            }

            WavelengthCorrection wlc = null;
            try
            { 
                if (is_zenith)
                    wlc = new WavelengthCorrection(master_oxid, oxid, SpectrumProcessor.ZenithRecalibrationWavelengths, false, this.levenberg_marquardt_options);
                else
                    wlc = new WavelengthCorrection(master_oxid, oxid, null, true, this.levenberg_marquardt_options);
            }
            catch(Spectroscopy.Math.LevenbergMarquardt.MaxIterationNumberException ex)
            {
                switch (this.MaxIterationReachedBehavior)
                {
                    case WlcMaxIterationReachedBehavior.Ignore:
                        return new WavelengthCorrection();
                    case WlcMaxIterationReachedBehavior.RaiseEventAndIgnore:
                        this.WavelengthCorrectionMaxIterationReached?.Invoke(this, new WlcMaxIterationReachedEventArgs(correction_of_reference, 
                                                                                                                       this.levenberg_marquardt_max_iterations, 
                                                                                                                       ex));
                        return new WavelengthCorrection();
                    case WlcMaxIterationReachedBehavior.ThrowException:
                        throw ex;
                }
            }
            return wlc;
        }

        public void ForceUpdateWavelengthCorrection(bool wavelength_correction_of_sample)
        {
            if (wavelength_correction_of_sample)
                this.UpdateOxidOfSample();
            else
                this.UpdateOxidOfReference();
        }

        private void UpdateLevenbergMarquardtOptions()
        { 
            this.levenberg_marquardt_options = new Math.LevenbergMarquardt.OptimParams()
            {
                MaxIterations = this.levenberg_marquardt_max_iterations,
                ToleranceArgument = this.levenberg_marquardt_tolerance,
                ToleranceGradient = this.levenberg_marquardt_tolerance,
                ToleranceFunction = this.levenberg_marquardt_tolerance,
            };
        }

        public static DataSet ConvertWhiteReference(WhiteReferenceConversionMode mode, DataSet corrected_intensity)
        {
            if (SpectrumProcessor.factor_tio2_to_zenith == null)
                SpectrumProcessor.InitFactorTiO2ToZenith();

            switch (mode)
            {
                case WhiteReferenceConversionMode.None:
                    return corrected_intensity;

                case WhiteReferenceConversionMode.TitaniumdioxideToZenith:
                    return corrected_intensity / SpectrumProcessor.factor_tio2_to_zenith.Interpolate(corrected_intensity.X);

                case WhiteReferenceConversionMode.ZenithToTitaniumdioxide:
                    throw new NotSupportedException("Conversion Zenith => TiO2 is not supported anymore.");
            }

            throw new ArgumentException(Catalog.GetString("Unknown conversion mode:") + " " + mode.ToString());
        }

        private static void InitFactorTiO2ToZenith()
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            Spectrum tio2_average = CSV.Read(assembly.GetManifestResourceStream("Spectroscopy.Processing.WhiteReferences.average_reflectance_tio2.csv"));
            Spectrum zenith_average = CSV.Read(assembly.GetManifestResourceStream("Spectroscopy.Processing.WhiteReferences.average_reflectance_zenith.csv"));

            DataSet tio2_reflectance = tio2_average.Intensity / tio2_average.Reference.Intensity;
            DataSet zenith_reflectance = zenith_average.Intensity / zenith_average.Reference.Intensity;

            SpectrumProcessor.factor_tio2_to_zenith = tio2_reflectance / zenith_reflectance;
        }

        private void UpdateProcessingBackground()
        {
            DataSet black = this.ProcessingDark;

            if (black == null)
            {
                this.external_background_type = ExternalBackgroundType.Unknown;
                this.processing_external_background = null;
            }
            else
            {
                DataSet white = null;
                ExternalBackgroundType type = ExternalBackgroundType.Unknown;

                if (this.ProcessingWhite != null && this.ProcessingSampleInsertWhite == null && this.ProcessingTransflectanceInsert == null)
                { 
                    white = new DataSet(this.ProcessingWhite.RawIntensity);
                    type = ExternalBackgroundType.White;
                }
                else if (this.ProcessingWhite == null && this.ProcessingSampleInsertWhite != null && this.ProcessingTransflectanceInsert == null)
                { 
                    white = new DataSet(this.ProcessingSampleInsertWhite.RawIntensity);
                    type = ExternalBackgroundType.SampleInsertWhite;
                }
                else if (this.ProcessingSampleInsertWhite == null && this.ProcessingTransflectanceInsert != null)
                { 
                    white = new DataSet(this.ProcessingTransflectanceInsert.RawIntensity);
                    type = ExternalBackgroundType.TransflectanceWhite;
                }
                else
                {
                    this.external_background_type = ExternalBackgroundType.Unknown;
                    throw new Exception(Catalog.GetString("Cannot find matching background spectrum for processing"));
                }

                this.external_background_type = type;
                this.processing_external_background = white;
                this.processing_external_background -= black;
            }
        }

        private void UpdateProcessingBlack()
        {
            if (this.ProcessingBlack != null)
                this.processing_external_dark = new DataSet(this.ProcessingBlack.RawIntensity);
            else
                this.processing_external_dark = null;
        }

        protected void LimitingIntensities(DataSet intensity)
        {
            List<int> indices = intensity.Find('Y', '<', this.IntensityLimit);

            if (indices.Count > 0)
            {
                foreach (int i in indices)
                {
                    intensity.Y[i] = this.IntensityLimit;
                }
            }
        }

        protected void TransformSpectrum(Spectrum spectrum)
        {
            switch (this.TransformationType)
            {
                case TransformationTypes.FinderSD_ApoIdent:
                    FinderSDApoIdentTransformer.ApplyTransformation(spectrum);
                    break;

                case TransformationTypes.FinderX_FinderSD:
                    FinderXFinderSDTransformer.ApplyTransformation(spectrum);
                    break;

                default:
                    break;
            }
        }

        protected void ConfigureLimitBandwidth(Spectrum spectrum, bool bandwidth_mode_is_raw_spectrum = true)
        {
            if (spectrum == null)
                return;

            LimitBandwidthMode mode = bandwidth_mode_is_raw_spectrum ? this.RawSpectrumLimitBandwidthMode : this.ProcessedSpectrumLimitBandwidthMode;

            switch (mode)
            {
                case LimitBandwidthMode.ForceSwitchOn:
                    spectrum.LimitBandwidth = true;
                    break;
                case LimitBandwidthMode.ForcedSwitchOff:
                    spectrum.LimitBandwidth = false;
                    break;
                
                default:
                    break;
            }
        }

        public Spectrum CreateProcessedSampleSpectrum(Spectrum corrected_sample_spectrum, Spectrum raw_sample_spectrum, DataSet intensity, DataSet background)
        {
            Spectrum result = new Spectrum(intensity, corrected_sample_spectrum.WavelengthLimits)
            {
                Reference = new Spectrum(background, corrected_sample_spectrum.WavelengthLimits),
                DarkIntensity = new DarkIntensity(double.Epsilon, this.sample_dark_intensity.Sigma),
                Id = raw_sample_spectrum.Id + "_processed",
                Label = raw_sample_spectrum.Label + " " + Catalog.GetString("(processed)"),
                Creator = this.NoCreatorUpdate ? raw_sample_spectrum.Creator : this.Creator,
                Timestamp = raw_sample_spectrum.Timestamp,
                Serial = raw_sample_spectrum.Serial,
                FirmwareVersion = raw_sample_spectrum.FirmwareVersion,
                FirmwareRevision = raw_sample_spectrum.FirmwareRevision,
                LutTimestamp = raw_sample_spectrum.LutTimestamp,
                HardwareInfo = raw_sample_spectrum.HardwareInfo,
                AverageCount = raw_sample_spectrum.AverageCount,
                WavelengthCorrection = corrected_sample_spectrum.WavelengthCorrection,
                LinearDistortion = corrected_sample_spectrum.LinearDistortion,
                WavelengthLimits = corrected_sample_spectrum.WavelengthLimits,
                SpectralResolution = corrected_sample_spectrum.SpectralResolution,
                WlcMasterSpectrumId = corrected_sample_spectrum.WlcMasterSpectrumId,
                Comment = string.IsNullOrEmpty(corrected_sample_spectrum.Comment) ? string.Empty : corrected_sample_spectrum.Comment + "\n"
            };

            result.Comment += "origin_has_adc_correction=" + raw_sample_spectrum.HasAdcCorrection.ToString() + "\n";
            result.SpectrumType = this.IsFluid ? SpectrumType.ProcessedFluidSpecimen : SpectrumType.ProcessedSolidSpecimen;
            result.ProbeType = corrected_sample_spectrum.ProbeType;

            return result;
        }

        protected static void SetRawspectrumData(Spectrum spectrum,
                                                 Spectrum background,
                                                 DarkIntensity dark_intensity,
                                                 WavelengthCorrection wavelength_correction,
                                                 bool use_default_system_function)
        {
            spectrum.Reference = background?.Clone();

            if (dark_intensity != null)
            {
                if (spectrum.Reference != null)
                    spectrum.Reference.DarkIntensity = new DarkIntensity(dark_intensity);
                spectrum.DarkIntensity = new DarkIntensity(dark_intensity);
            }

            if (wavelength_correction != null)
                spectrum.WavelengthCorrection = new WavelengthCorrection(wavelength_correction);


            if (use_default_system_function && spectrum.HasSystemFunctionCorrection == false)
                spectrum.SystemFunctionCorrection = DataSet.GetDefaultSystemFunctionCorrectionCoefficients();

        }

        protected void SetReferenceRawspectrumData(Spectrum spectrum)
        {
            if (this.wavelength_correction_of_references == null)
                this.UpdateOxidOfReference();

            SpectrumProcessor.SetRawspectrumData(spectrum,
                                            this.reference_background,
                                            this.reference_dark_intensity,
                                            this.wavelength_correction_of_references,
                                            this.use_default_system_function);

            if (spectrum.HasWavelengthCorrection)
                spectrum.WlcMasterSpectrumId = this.reference_wavelength_correction_id;

            this.ConfigureLimitBandwidth(spectrum);
        }

        public void SetSampleRawspectrumData(Spectrum spectrum)
        {
            if (this.wavelength_correction_of_sample == null)
                this.UpdateOxidOfSample();

            SpectrumProcessor.SetRawspectrumData(spectrum,
                                            this.sample_background,
                                            this.sample_dark_intensity,
                                            this.wavelength_correction_of_sample,
                                            this.use_default_system_function);

            if (spectrum.HasWavelengthCorrection)
                spectrum.WlcMasterSpectrumId = this.sample_wavelength_correction_id;

            this.ConfigureLimitBandwidth(spectrum);
        }

        protected bool IsRawspectrumComplete(Spectrum spectrum, bool need_wavelength_correction = true)
        {
            return spectrum.HasReference && spectrum.HasDarkIntensity && (need_wavelength_correction == false || spectrum.HasWavelengthCorrection);
        }

        public static List<double> ZenithRecalibrationWavelengths
        {
            get
            {
                if (SpectrumProcessor.zenith_recalibration_wavelengths == null)
                {
                    SpectrumProcessor.zenith_recalibration_wavelengths = new List<double>();
                    for (int ix = 1020; ix <= 1310; ++ix)
                    {
                        SpectrumProcessor.zenith_recalibration_wavelengths.Add(ix);
                    }
                    for (int ix = 1410; ix <= 1890; ++ix)
                    {
                        SpectrumProcessor.zenith_recalibration_wavelengths.Add(ix);
                    }
                }

                return SpectrumProcessor.zenith_recalibration_wavelengths;
            }
        }
    }
}
