// Created with MonoDevelop
//
//    Copyright (C) 2013 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 Hiperscan.SGS.Common;
using Hiperscan.Spectroscopy;
using Hiperscan.Unix;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.Spectroscopy.ReferenceChecks;

namespace Hiperscan.SGS.Benchtop
{
    public partial class StabilizedFinderX : IDisposable
    {
        public class LowTemperatureException : System.Exception
        {
            public LowTemperatureException(string msg) 
                : base(msg)
            {
            }
        }

        public class ReferencingTimeoutException : System.Exception
        {
            public ReferencingTimeoutException()
            {
            }

            public ReferencingTimeoutException(string msg) : base(msg)
            {
            }

            public ReferencingTimeoutException(string msg, Exception inner) : base(msg, inner)
            {
            }
        }

        private const int DEFAULT_FLUID_POSITION = MotorPositions.Sample1;

        protected IdleTaskHandler idle_task_handler = null;

        private volatile bool is_initialized = false;
        private volatile bool ignore_low_temperature = false;
        private volatile bool disposed = false;

        private Spectrum dark_master;
        private Spectrum white_master;
        private double di0;
        private double sigma0;
        private double add_data_di0;
        private string creator_name = String.Empty;


        protected volatile bool auto_finder_light = true;

        public StabilizedFinderX(IDevice dev, Spectroscopy.Processing.ReferenceWheelVersion wheel_version, string creator_name)
        {
            this.Device = dev;
            //this.Finder = new Finder(null, (Benchtop.ReferenceWheelVersion)wheel_version, ExternalWhiteReferenceType.Zenith, creator_name);

            this.ProbeType = ProbeType.FinderXLineScan;
            this.SpectrumProcessor = new Spectroscopy.Processing.ReferenceBlockProcessor(creator_name);
            this.ProcessorForCorrection = new Spectroscopy.Processing.ReferenceBlockProcessor(creator_name);
            this.SpectrumProcessor.ReferenceWheelVersion = wheel_version;
            this.ProcessorForCorrection.ReferenceWheelVersion = wheel_version;

            this.ReferenceChecks = new ReferenceChecks(this.ProcessorForCorrection);
            this.ReferenceChecks.FailureMethod += this.ResetReferences;

            this.AutonomousMode = true;
            this.AutonomousTargetTemperature = 15.0;
            this.ReferencingMaxDuration = new TimeSpan(0, 1, 30);

            this.ExternalCalibrationFailed += () => this.Device.SetFinderPosition(MotorPositions.Standby, this.idle_task_handler);
        }

        public void Init()
        {
            if (this.Device.Info.VendorId != VendorId.HIPERSCAN && this.Device.Info.ProductId != ProductId.ES22_SENSOR_FINDERX)
                throw new NotSupportedException(Catalog.GetString("The device does not support active temperature control."));

            double current_temperature = this.Device.ReadStatus();
            //TODO Target Temperature

            this.Device.SetFinderPosition(MotorPositions.Standby, this.idle_task_handler);
            this.Device.ReadLightSourceVoltages();
            ushort lamp_voltage = this.Device.LightSourceSettings.Lamp3Voltage_mV;
            LightSourceControl.LightSourceConfig light_source_config = new LightSourceControl.LightSourceConfig()
            {
                LampSelection = LightSourceControl.LampSelection.All,
                SetLampVoltage_mV = lamp_voltage
            };

            this.Device.SetLightSourceConfig(light_source_config);
            this.Device.SetFinderLightSource(true, null);
            this.WaitForLightSource(this.LightSourceWaitSpan);

            this.Device.SetFinderPosition(MotorPositions.Fix2GlassWhite, this.idle_task_handler);
            this.white_master = this.Device.Single(this.idle_task_handler);

            ushort save_average = this.Device.CurrentConfig.Average;
            this.Device.CurrentConfig.Average = this.ReferencingAverage;
            this.Device.WriteConfig();

            this.Device.SetFinderLightSource(false, null);
            System.Threading.Thread.Sleep(TimeSpan.FromMilliseconds(500.0));
            this.dark_master = this.Device.Single(this.idle_task_handler);
            this.Device.SetFinderLightSource(true, null);
            this.Device.SetFinderPosition(MotorPositions.Standby, this.idle_task_handler);

            this.dark_master.SpectrumType = SpectrumType.InternalDark;
            this.SaveReferenceSpectrum(this.dark_master, SpectrumType.InternalDark, false, "intdarkmaster");
            this.di0 = this.dark_master.RawIntensity.YMean();
            this.sigma0 = this.dark_master.RawIntensity.YSigma();
            this.add_data_di0 = (double)(this.white_master.AddData[2]) * 1e-4;

            this.white_master.DarkIntensity = new DarkIntensity(this.di0, this.sigma0);
            this.white_master.SpectrumType = SpectrumType.Fix2GlassWhite;
            this.SaveReferenceSpectrum(this.white_master, SpectrumType.Fix2GlassWhite, false, "intwhitemaster");

            this.Device.CurrentConfig.Average = save_average;
            this.Device.WriteConfig();

            this.WaitForLightSource(this.LightSourceWaitSpan);

            this.auto_finder_light = false;
            this.is_initialized = true;

        }

        public void WaitForLightSource(TimeSpan max_timespan)
        {
            this.Device.ReadLightSourceState();
            if (this.Device.CurrentLightSourceState.State != LightSourceControl.LightSourceState.On)
                return;

            DateTime then = DateTime.Now;

            while ((DateTime.Now - then) < max_timespan)
            {
                this.Device.ReadLightSourceState();
                LightSourceControl.LightSourceState state = this.Device.CurrentLightSourceState.State;
                if (state == LightSourceControl.LightSourceState.OnAndStabilized)
                    break;

                this.OnWaitForLightSource?.Invoke(then + max_timespan - DateTime.Now);

                for (int ix = 0; ix < 5; ++ix)
                {
                    System.Threading.Thread.Sleep(200);
                    this.idle_task_handler?.Invoke();
                }
            }
        }

        public void Deinit()
        {
            Console.WriteLine("Deinit()");
            try
            {
                this.Device.SetFinderPosition(FinderWheelPosition.Standby, this.idle_task_handler);
            }
#pragma warning disable RECS0022
            catch { }
#pragma warning restore RECS0022
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        public virtual void Dispose(bool disposing)
        {
            if (this.disposed == false)
            {
                if (disposing)
                {
                    this.Deinit();
                    Console.WriteLine("Object disposed.");
                }

                this.disposed = true;
            }
        }

        public IdleTaskHandler IdleTaskHandler
        {
            set { this.idle_task_handler = value; }
        }

        public void Acquire(bool fluid)
        {
            if (this.is_initialized == false)
                throw new Exception(Catalog.GetString("Cannot acquire spectrum with uninitialised device. Call Init() first."));

            try
            { 
                this.SetFinderStatus(FinderStatus.AcquireSample);

                if (fluid)
                {
                    this.Device.SetFinderPosition(StabilizedFinderX.DEFAULT_FLUID_POSITION, this.idle_task_handler);
                    this.CurrentSampleSpectrum = this.Device.Single(this.auto_finder_light, this.idle_task_handler);
                }
                else
                {
                    this.Device.SetFinderPosition(MotorPositions.LeftSample, this.idle_task_handler);
                    this.CurrentSampleSpectrum = this.Device.SingleLineScan(this.Device.CurrentConfig.Average, 
                                                                     Spectrometer.TriggerType.TripletScanCustom, 
                                                                     this.auto_finder_light, 
                                                                     this.idle_task_handler);
                }
                this.CurrentSampleSpectrum.ProbeType = this.ProbeType;
                this.ProcessorForCorrection.SetSampleRawspectrumData(this.CurrentSampleSpectrum);

            }
            catch 
            {
                this.CurrentSampleSpectrum = null;
                this.Device.SetFinderPosition(MotorPositions.Standby, this.idle_task_handler);
                this.SetFinderStatus(FinderStatus.Idle);
                throw;
            }
        }

        public void AcquireFixTwoGlassReferences(bool pre_sampling_references)
        {
            bool success = false;
            DateTime referencing_started = DateTime.Now;

            if (this.Device == null)
                throw new Exception(Catalog.GetString("No device found"));

            this.ExternalCalibrationStarted?.Invoke();
            ushort save_average = this.Device.CurrentConfig.Average;
            try
            { 
                this.Device.CurrentConfig.Average = this.ReferencingAverage;
                this.Device.WriteConfig();

                Spectrum black = null;
                Spectrum white = null;
                Spectrum oxide = null;

                if (pre_sampling_references)
                {
                    black = this.AcquireReferenceSpectrum(SpectrumType.Fix2GlassBlack);
                    oxide = this.AcquireReferenceSpectrum(SpectrumType.Fix2GlassOxid);
                    white = this.AcquireReferenceSpectrum(SpectrumType.Fix2GlassWhite);

                    this.DarkSpectrum = this.CalculateDarkSpectrum(black, white);
                    this.ProcessorForCorrection.DarkReference = this.DarkSpectrum;

                    this.ProcessorForCorrection.FixTwoGlassBlackReference = black;
                    this.ProcessorForCorrection.FixTwoGlassOxideReference = oxide;
                    this.ProcessorForCorrection.FixTwoGlassWhiteReference = white;

                    black = this.ProcessorForCorrection.FixTwoGlassBlackReference;
                    white = this.ProcessorForCorrection.FixTwoGlassWhiteReference;
                    oxide = this.ProcessorForCorrection.FixTwoGlassOxideReference;

                    this.BlackSpectrumPast = black;
                    this.WhiteSpectrumPast = white;
                    this.OxideSpectrumPast = oxide;

                }
                else
                {
                    if (this.DarkSpectrum == null)
                        throw new Exception(Catalog.GetString("Cannot acquire reference spectrum without previously sampling dark reference"));

                    white = this.AcquireReferenceSpectrum(SpectrumType.Fix2GlassWhite);
                    oxide = this.AcquireReferenceSpectrum(SpectrumType.Fix2GlassOxid);
                    black = this.AcquireReferenceSpectrum(SpectrumType.Fix2GlassBlack);

                    this.ProcessorForCorrection.FixTwoGlassBlackReference = black;
                    this.ProcessorForCorrection.FixTwoGlassOxideReference = oxide;
                    this.ProcessorForCorrection.FixTwoGlassWhiteReference = white;

                    black = this.ProcessorForCorrection.FixTwoGlassBlackReference;
                    white = this.ProcessorForCorrection.FixTwoGlassWhiteReference;
                    oxide = this.ProcessorForCorrection.FixTwoGlassOxideReference;

                    this.BlackSpectrumFuture = black;
                    this.WhiteSpectrumFuture = white;
                    this.OxideSpectrumFuture = oxide;
                }

                //FIXME Checks

                this.ReferenceChecks.CheckBlackReference(black);
                this.SaveReferenceSpectrum(black, SpectrumType.Fix2GlassBlack, false);
                this.ReferenceChecks.CheckWhiteReference(white);
                this.SaveReferenceSpectrum(white, SpectrumType.Fix2GlassWhite, false);
                this.SaveReferenceSpectrum(oxide, SpectrumType.Fix2GlassOxid, false);

                TimeSpan span = DateTime.Now - referencing_started;
                if (span > this.ReferencingMaxDuration)
                {
                    this.ResetReferences();
                    throw new ReferencingTimeoutException(Catalog.GetString("The reference procedure must be finished a short time period (max. 3 Minutes) after acquisition of the first spectrum. This condition was not fulfilled. Please repeat measurement."));
                }

                this.ReferencingTime = DateTime.Now;
                success = true;
            }
            catch (Exception)
            { 
                this.ExternalCalibrationFailed?.Invoke();

                this.ResetReferences();
                success = false;
                throw;
            }
            finally
            {
                this.Device.CurrentConfig.Average = save_average;
                this.Device.WriteConfig();
                this.SetFinderStatus(FinderStatus.Idle);
                this.ExternalCalibrationFinished?.Invoke(success);
            }
        }

        private void ResetReferences()
        {
            this.BlackSpectrumPast = null;
            this.BlackSpectrumFuture = null;
            this.WhiteSpectrumPast = null;
            this.WhiteSpectrumFuture = null;
            this.OxideSpectrumPast = null;
            this.OxideSpectrumFuture = null;
        }

        private Spectrum AcquireReferenceSpectrum(SpectrumType spectrum_type)
        {
            int motor_position;
            FinderStatus status;

            switch (spectrum_type)
            {
                case SpectrumType.Fix2GlassOxid:
                    motor_position = MotorPositions.Fix2GlassOxid;
                    status = FinderStatus.Recalibration;
                    break;

                case SpectrumType.Fix2GlassBlack:
                    motor_position = MotorPositions.Fix2GlassBlack;
                    status = FinderStatus.AcquireBlackReference;
                    break;

                case SpectrumType.Fix2GlassWhite:
                    motor_position = MotorPositions.Fix2GlassWhite;
                    status = FinderStatus.AcquireWhiteReference;
                    break;

                default:
                    throw new Exception(string.Format(Catalog.GetString("Invalid spectrum type for acquiring reference spectrum: {0}"), spectrum_type));
            }

            this.SetFinderStatus(status);
            this.Device.SetFinderPosition(motor_position, this.idle_task_handler);

            Spectrum spectrum = this.Device.Single(this.auto_finder_light, this.idle_task_handler);
            spectrum.ProbeType = this.ProbeType;
            spectrum.SpectrumType = spectrum_type;

            return spectrum;
        }

        public Spectrum CalculateDarkSpectrum(Spectrum black, Spectrum white)
        {
            double di = ((double)white.AddData[2] * 1e-4) - this.add_data_di0 + this.di0;
            DataSet new_sigma = black.RawIntensity - black.RawIntensity.Filter(3,51);
            new_sigma -= new_sigma.YMean();

            Spectrum dark = this.dark_master.Clone();
            dark.RawIntensity *= 0.0;
            dark.RawIntensity += di;
            dark.RawIntensity += new_sigma;

            return dark;
        }

    }
}
