// Created with MonoDevelop
//
//    Copyright (C) 2019 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.Linq;
using System.Runtime.InteropServices;
using System.Threading;

using Hiperscan.Common;
using Hiperscan.SGS.Benchtop;
using Hiperscan.SGS.Common;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.SGS.SerialInterface.USB;
using Hiperscan.SGS.SgsNt.Protocol;
using Hiperscan.Spectroscopy;
using Hiperscan.Unix;

using Version = Hiperscan.SGS.Common.Version;


namespace Hiperscan.SGS.SgsNt
{

    public class Prototype : Hiperscan.SGS.Sgs1900.BaseDevice
    {
        public new const int LUT_LENGTH = 2800;

        protected class Protocol : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol
        {
            public static readonly Command Connect            = new Command(AddressType.Master, 'S', 'C', null, 0, null);
            public static readonly Command Disconnect         = new Command(AddressType.Master, 'D', 'C', null, 0, null);
            public static readonly Command AcquireSpectrum    = new Command(AddressType.Master, 'A', 'S', typeof(byte[]), 3, typeof(byte));
            public static readonly Command ReadIntensity      = new Command(AddressType.Master, 'R', 'I', typeof(byte), sizeof(byte), typeof(IntensityReadData));
            public static readonly Command SetMaintenanceMode = new Command(AddressType.Master, 'M', 'M', typeof(byte[]), 20, null);
            public static readonly Command ReadLookupTable    = new Command(AddressType.Master, 'R', 'L', typeof(byte), sizeof(byte), typeof(LutReadData));
            public static readonly Command WriteLookupTable   = new Command(AddressType.Master, 'L', 'W', typeof(LutWriteData), (UInt16)Marshal.SizeOf(typeof(LutWriteData)), null);
            public static readonly Command ReadMsaFactor      = new Command(AddressType.Master, 'S', 'Y', null, 0, typeof(UInt16));
            public static readonly Command WriteMsaFactor     = new Command(AddressType.Master, 'S', 'X', typeof(UInt16), sizeof(UInt16), null);
            public static readonly Command ReadFirmwareInfo   = new Command(AddressType.Master, 'V', 'R', null, 0, typeof(FirmwareInfoReadData));

            public static readonly Command DetectPeripheral   = new Command(AddressType.Peripheral, 'F', 'D', null, 0, typeof(PeripheralInfoReadData));

            // TODO: move this commands to a PPNT class
            public static readonly Command PeripheralConnect          = new Command(AddressType.Peripheral, 'S', 'C', null, 0, typeof(byte));
            public static readonly Command PeripheralSetWheelPosition = new Command(AddressType.Peripheral, 'F', 'P', typeof(byte), sizeof(byte), typeof(byte));
            public static readonly Command PeripheralSetLightSource   = new Command(AddressType.Peripheral, 'F', 'L', typeof(byte), sizeof(byte), typeof(byte));
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        protected struct FirmwareInfoReadData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.U2)] public UInt16 Major;
            [MarshalAs(UnmanagedType.U2)] public UInt16 Minor;
            [MarshalAs(UnmanagedType.U2)] public UInt16 Patch;
            [MarshalAs(UnmanagedType.U2)] public UInt16 Build;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        protected struct PeripheralInfoReadData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
            public byte[] VersionBytes;

            [MarshalAs(UnmanagedType.U1)] public byte MajorProtocolVersion;
            [MarshalAs(UnmanagedType.U1)] public byte MinorProtocolVersion;
            [MarshalAs(UnmanagedType.U1)] public byte MajorFirmwareVersion;
            [MarshalAs(UnmanagedType.U1)] public byte MinorFirmwareVersion;
            [MarshalAs(UnmanagedType.U1)] public byte PeripheralId;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        protected struct LutReadData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.U8)] public UInt64 Timestamp;
            [MarshalAs(UnmanagedType.U4)] public UInt32 SkipCount;
            [MarshalAs(UnmanagedType.R4)] public float  MinWavelength;
            [MarshalAs(UnmanagedType.R4)] public float  MaxWavelength;
            [MarshalAs(UnmanagedType.R4)] public float  Increment;
            [MarshalAs(UnmanagedType.R4)] public float  ExtensionRange;
            [MarshalAs(UnmanagedType.U4)] public UInt32 DarkIntensityEndpoint;
            [MarshalAs(UnmanagedType.U4)] public UInt32 Length;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2800)]
            public float[] Data;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        protected struct LutWriteData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.U1)] public byte Channel;
            [MarshalAs(UnmanagedType.U8)] public UInt64 Timestamp;
            [MarshalAs(UnmanagedType.U4)] public UInt32 SkipCount;
            [MarshalAs(UnmanagedType.R4)] public float  MinWavelength;
            [MarshalAs(UnmanagedType.R4)] public float  MaxWavelength;
            [MarshalAs(UnmanagedType.R4)] public float  Increment;
            [MarshalAs(UnmanagedType.R4)] public float  ExtensionRange;
            [MarshalAs(UnmanagedType.U4)] public UInt32 DarkIntensityEndpoint;
            [MarshalAs(UnmanagedType.U4)] public UInt32 Length;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2800)]
            public float[] Data;

            public LutWriteData(Spectrometer.LUT lut) : this()
            {
                this.Channel = lut.AcquisitionChannelMask;
                this.Timestamp = (ulong)lut.Timestamp.Ticks;
                this.SkipCount = lut.SkipCount;
                this.MinWavelength = (uint)lut.MinWavelength;  // FIXME: remove cast if type is corrected
                this.MaxWavelength = (uint)lut.MaxWavelength;
                this.Increment = (uint)lut.Increment;
                this.ExtensionRange = (uint)lut.ExtensionRange;
                this.DarkIntensityEndpoint = lut.DarkIntensityEndpoint;
                this.Length = (uint)lut.Length;

                this.Data = new float[lut.Length];
                Array.Copy(lut.Data, this.Data, lut.Length);
            }

            public LutWriteData(Protocol.ChannelType channel, LutReadData lut_read_data) : this()
            {
                this.Channel = (byte)channel;
                this.Timestamp = lut_read_data.Timestamp;
                this.SkipCount = lut_read_data.SkipCount;
                this.MinWavelength = lut_read_data.MinWavelength;
                this.MaxWavelength = lut_read_data.MaxWavelength;
                this.Increment = lut_read_data.Increment;
                this.ExtensionRange = lut_read_data.ExtensionRange;
                this.DarkIntensityEndpoint = lut_read_data.DarkIntensityEndpoint;
                this.Length = lut_read_data.Length;

                this.Data = new float[this.Length];
                Array.Copy(lut_read_data.Data, this.Data, lut_read_data.Length);
            }
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        internal struct IntensityReadData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.R4)] public float MemsFrequency;
            [MarshalAs(UnmanagedType.R4)] public float Temperature;
            [MarshalAs(UnmanagedType.R4)] public float DarkIntensity;
            [MarshalAs(UnmanagedType.U1)] public byte  AcquisitionChannelMask;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2592)]
            public float[] Intensities;
        }


        private const double AVERAGING_WINDOW_SIZE  = 5.0;
        private const double INTENSITY_SCALE_FACTOR = 1e-3;

        private readonly Dictionary<Protocol.ChannelType,Spectrometer.LUT> lookup_tables = new Dictionary<BaseProtocol.ChannelType, Spectrometer.LUT>();
        private readonly Protocol.ChannelType[] ACTIVE_CHANNELS = new BaseProtocol.ChannelType[]
        {
            BaseProtocol.ChannelType.Channel1,
            BaseProtocol.ChannelType.Channel2,
            BaseProtocol.ChannelType.Channel3,
            BaseProtocol.ChannelType.Channel1 | BaseProtocol.ChannelType.ReverseScan,
            BaseProtocol.ChannelType.Channel2 | BaseProtocol.ChannelType.ReverseScan,
            BaseProtocol.ChannelType.Channel3 | BaseProtocol.ChannelType.ReverseScan
        };

        private readonly string serial;
        private double scan_engine_msa_factor = double.NaN;
        private bool has_periphal;

        internal Prototype(DeviceInfo info, VersionInfo version) : base(info, version)
        {
            if (this.serial_interface != null)
                return;

            try
            {
                this.serial_interface = new CP2130(info.VendorId, info.ProductId);
                this.SetState(Spectrometer.State.Disconnected);
                this.serial = info.SerialNumber;
            }
            catch (Exception ex)
            {
                ExceptionHandler(Catalog.GetString("Cannot initialize USB communication with device:") + " " + this.info.SerialNumber, ex);
            }
        }

        public override void Open(bool auto_init)
        {
            lock (this.opening_mutex)
            {
                this.IgnoreSgsExceptions = false;

                try
                {
                    if (this.GetState() == Spectrometer.State.Disconnected)
                    {
                        // initialize usb device
                        try
                        {
                            this.serial_interface.OpenBySerialNumber(this.info.SerialNumber);
                        }
                        catch (System.Runtime.Remoting.RemotingException ex)
                        {
                            throw ex;
                        }

                        this.SetState(Spectrometer.State.Idle);

                        if (auto_init == false)
                            return;

                        foreach (var channel in ACTIVE_CHANNELS)
                        {
                            if (this.lookup_tables.ContainsKey(channel))
                                continue;

                            this.lookup_tables.Add(channel, this.ReadLookupTable(channel));
                        }
                        this.HardwareProperties.MinWavelength = this.lookup_tables.Values.Select(l => l.MinWavelength).Min();
                        this.HardwareProperties.MaxWavelength = this.lookup_tables.Values.Select(l => l.MaxWavelength).Max();

                        this.scan_engine_msa_factor = this.ReadScanEngineMsaFactor();

                        try
                        {
                            this.Connect();
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Cannot connect device: {0}", ex.Message);
                            Console.WriteLine("Trying to reset and reopen device...");
                            this.serial_interface.ResetDevice();
                            this.Connect();
                        }

                        FirmwareInfoReadData firmware_info = this.ReadFirmwareInfo();

                        this.firmware_version  = $"{firmware_info.Major}.{firmware_info.Minor}.{firmware_info.Patch}";
                        this.firmware_revision = $"{firmware_info.Build}";
                        this.lut_timestamp = this.lookup_tables.Values.First().Timestamp;
                        //this.clock_frequency = this.ReadClockFrequency();

                        PeripheralInfoReadData? peripheral_info = null;

                        if (this.has_periphal)
                            peripheral_info = this.ReadPeripheralInfo();

                        if (peripheral_info != null)
                        {
                            this.is_finder = true;
                            this.FinderVersion = new Benchtop.FinderSetup.FinderVersion(((PeripheralInfoReadData)peripheral_info).VersionBytes);
                        }

                        Console.WriteLine("Read system parameter:");
                        Console.WriteLine("Firmware version:\t" + this.firmware_version);
                        Console.WriteLine("Firmware revision:\t" + this.firmware_revision);
                        Console.WriteLine("LUT timestamp:\t" + this.lut_timestamp.ToString());
                    }
                }
                catch (System.Runtime.Remoting.RemotingException ex)
                {
                    this.SetState(Spectrometer.State.Disconnected);
                    throw ex;
                }
                catch
                {
                    this.SetState(Spectrometer.State.Disconnected);
                    try
                    {
                        this.serial_interface.Close();
                    }
#pragma warning disable RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist
                    catch { }
#pragma warning restore RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist

                    throw;
                }
                finally
                {
                    this.is_initialized = true;
                }
            }
        }

        private FirmwareInfoReadData ReadFirmwareInfo()
        {
            Protocol.Response response = this.WriteCommand(Protocol.ReadFirmwareInfo, this.current_config.Timeout);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot read firmware info."), this);

            return (FirmwareInfoReadData)Protocol.TransferStruct.FromBuffer(response.TransferStructValue, typeof(FirmwareInfoReadData));
        }

        private PeripheralInfoReadData? ReadPeripheralInfo()
        {
            Protocol.Response response = this.WriteCommand(Protocol.DetectPeripheral, this.current_config.Timeout);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot read peripheral info."), this);

            return (PeripheralInfoReadData)Protocol.TransferStruct.FromBuffer(response.TransferStructValue, typeof(PeripheralInfoReadData));
        }

        public override void SetMaintenanceMode(byte[] key)
        {
            Protocol.Command command = new Protocol.Command(Protocol.SetMaintenanceMode)
            {
                ByteArrayValue = key
            };

            Protocol.Response response = this.WriteCommand(command, this.current_config.Timeout);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot set maintenance mode."), this);
        }

        public override double ReadScanEngineMsaFactor()
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadMsaFactor);

            Protocol.Response response = this.WriteCommand(command, this.current_config.Timeout);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot set read Scan Engine MSA factor."), this);

            return (double)response.UInt16Value / 10000.0;
        }

        public override void WriteScanEngineMsaFactor(byte[] key, double msa_factor)
        {
            this.SetMaintenanceMode(key);

            Protocol.Command command = new Protocol.Command(Protocol.WriteMsaFactor)
            {
                UInt16Value = (UInt16)Math.Round(msa_factor * 10000.0)
            };

            Protocol.Response response = this.WriteCommand(command, 8000);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot write Scan Engine MSA factor."), this);
        }

        private Spectrometer.LUT ReadLookupTable(Protocol.ChannelType channel, IdleTaskHandler idle_task = null)
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadLookupTable)
            {
                ByteValue = (byte)channel
            };

            Protocol.Response response = this.WriteCommand(command, this.current_config.Timeout, idle_task);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot read lookup table."), this);

            LutReadData lrd = (LutReadData)Protocol.TransferStruct.FromBuffer(response.TransferStructValue, typeof(LutReadData));

            return new Spectrometer.LUT(lrd.Data,
                                        this.scan_engine_msa_factor,
                                        new DateTime((long)lrd.Timestamp),
                                        new Version(this.FirmwareVersion),
                                        this.ScanEngineVersion, 
                                        (byte)channel,
                                        lrd.SkipCount,
                                        lrd.MinWavelength,
                                        lrd.MaxWavelength,
                                        lrd.Increment,
                                        lrd.ExtensionRange,
                                        lrd.DarkIntensityEndpoint);
        }

        private void WriteLookupTable(byte[] key, LutWriteData lut_write_data, IdleTaskHandler idle_task = null)
        {
            this.SetMaintenanceMode(key);

            Protocol.Command command = new Protocol.Command(Protocol.WriteLookupTable)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(lut_write_data)
            };

            Protocol.Response response = this.WriteCommand(command, 5000, idle_task);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot write lookup table."), this);
        }

        public override void Close()
        {
            try
            {
                if (this.serial_interface != null)
                    this.serial_interface.Close();
            }
#pragma warning disable RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist
            catch { }
#pragma warning restore RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist

            this.SetState(Spectrometer.State.Disconnected);
        }

        protected override void Connect()
        {
            if (this.WriteCommand(Protocol.Connect, 1000) == null)
                throw new SgsException(Catalog.GetString("Cannot connect device."), this);

            BaseProtocol.Response response = this.WriteCommand(Protocol.PeripheralConnect, 1000, null, false);
            if (response  == null)
                throw new SgsException(Catalog.GetString("Cannot connect peripheral."), this);

            switch (response.HeaderErrno)
            {

            case BaseProtocol.Errno.Success:
                this.has_periphal = true;
                return;

            case BaseProtocol.Errno.NoSuchDevice:
                this.has_periphal = false;
                return;

            default:
                throw new SgsException(Catalog.GetString("Cannot connect peripheral."), this);

            }
        }

        internal Protocol.ChannelType AcquireSpectrum(UInt16 average_count, Protocol.ChannelType channel, IdleTaskHandler idle_task = null)
        {
            Protocol.Command command = new Protocol.Command(Protocol.AcquireSpectrum);

            byte[] payload_buf = BitConverter.GetBytes(average_count);
            payload_buf = payload_buf.Concat(new byte[] { (byte)channel }).ToArray();

            command.ByteArrayValue = payload_buf;

            Protocol.Response response = this.WriteCommand(command, this.CurrentConfig.Timeout, idle_task);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot aquire spectrum."), this);

            return (Protocol.ChannelType)response.ByteValue;
        }

        internal IntensityReadData ReadIntensity(Protocol.ChannelType channel, IdleTaskHandler idle_task = null)
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadIntensity)
            {
                ByteValue = (byte)channel
            };

            Protocol.Response response = this.WriteCommand(command, 1000, idle_task);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot read intensity."), this);

            return (IntensityReadData)Protocol.TransferStruct.FromBuffer(response.TransferStructValue, typeof(IntensityReadData));
        }

        internal override Spectrometer.Config ReadConfig(bool throw_if_invalid)
        {
            throw new NotImplementedException();
        }

        internal override void WriteConfig(Spectrometer.Config config)
        {
            this.SetState(Spectrometer.State.Idle);
            //          throw new System.NotImplementedException ();
        }

        public override void WriteEEPROM()
        {
            throw new NotImplementedException();
        }

        public override void StartSingle(bool auto_finder_light)
        {
            Thread run_single = new Thread(new ParameterizedThreadStart(this.DoRunSingle));
            run_single.Start(auto_finder_light);
        }

        private void ReadSpectrum(out double[] lambdas, out double[] intensities, out uint[] add_data, out Spectrometer.LUT lut, IdleTaskHandler idle_task)
        {
            string default_channel = Environment.GetEnvironmentVariable(Env.HCL.SGSNT_DEFAULT_ACQUISITION_CHANNEL) ?? "0";

            Protocol.ChannelType channel = BaseProtocol.ChannelType.Channel1;

            switch (default_channel)
            {

            case "0":
                channel = BaseProtocol.ChannelType.Channel1;
                break;

            case "1":
                channel = BaseProtocol.ChannelType.Channel1 | BaseProtocol.ChannelType.ReverseScan;
                break;

            case "2":
                channel = BaseProtocol.ChannelType.Channel2;
                break;

            case "3":
                channel = BaseProtocol.ChannelType.Channel2 | BaseProtocol.ChannelType.ReverseScan;
                break;

            case "4":
                channel = BaseProtocol.ChannelType.Channel3;
                break;

            case "5":
                channel = BaseProtocol.ChannelType.Channel3 | BaseProtocol.ChannelType.ReverseScan;
                break;
            case "6":
                channel = BaseProtocol.ChannelType.Channel4;
                break;

            case "7":
                channel = BaseProtocol.ChannelType.Channel4 | BaseProtocol.ChannelType.ReverseScan;
                break;

            default:
                break;

            }

            if (this.lookup_tables.ContainsKey(channel) == false)
                throw new KeyNotFoundException(string.Format(Catalog.GetString("Cannot find Lookup Table for acquisition channel {0}."),
                                                             channel));

            Spectrometer.LUT _lut = this.lookup_tables[channel];

            this.AcquireSpectrum(this.CurrentConfig.Average, channel, idle_task);

            IntensityReadData read_data = this.ReadIntensity(channel, idle_task);
            DataSet raw_intensity = new DataSet(this.lookup_tables[channel].Data.Take(read_data.Intensities.Length),
                                                read_data.Intensities);

            int start = (int)Math.Round((double)(_lut.MinWavelength - _lut.ExtensionRange) / (double)_lut.Increment);
            int count = (int)Math.Round((double)(_lut.MaxWavelength - _lut.MinWavelength + 2 * _lut.ExtensionRange + 1) / (double)_lut.Increment);
            lambdas = Enumerable.Range(start, count).Select(v => (double)v * (double)_lut.Increment).ToArray();

            intensities = new double[lambdas.Length];
            for (int ix = 0; ix < lambdas.Length; ++ix)
            {
                double lambda = lambdas[ix];
                intensities[ix] = raw_intensity.AsEnumerable()
                                               .Where(v => v.Item1 > lambda - AVERAGING_WINDOW_SIZE / 2.0 && v.Item1 < lambda + AVERAGING_WINDOW_SIZE / 2.0)
                                               .Average(v => v.Item2) * INTENSITY_SCALE_FACTOR;
            }

            lut = _lut;
            add_data = new uint[]
            {
                (uint)(Math.Round(read_data.MemsFrequency*100.0)),
                (uint)(Math.Round(read_data.Temperature*100.0)),
                (uint)(Math.Round(read_data.DarkIntensity*100.0)),
                (uint)read_data.AcquisitionChannelMask
            };
        }

        public override Spectrum Single(bool auto_finder_light, IdleTaskHandler idle_task)
        {
            try
            {
                Spectrum spectrum = null;

                if (this.GetState() == Spectrometer.State.Continuous ||
                    this.GetState() == Spectrometer.State.Stream     ||
                    this.GetState() == Spectrometer.State.QuickStream)
                {
                    while (spectrum == null)
                    {
                        lock (this.current_spectrum_mutex)
                            spectrum = new Spectrum(this.current_spectrum);

                        Thread.Sleep(50);
                    }
                }
                else
                {
                    if (this.is_finder && auto_finder_light)
                        this.SetFinderLightSource(true, idle_task);

                    if (!this.SetState(Spectrometer.State.Single))
                        throw new Exception(Catalog.GetString("Device is not ready."));

                    this.ReadSpectrum(out double[] lambda, 
                                      out double[] intensity, 
                                      out uint[] add_data, 
                                      out Spectrometer.LUT lut, 
                                      idle_task);

                    this.SetState(Spectrometer.State.Idle);

                    if ((lambda.Length < 2) || (lambda.Length != intensity.Length))
                    {
                        Console.WriteLine("Read spectrum is not valid");
                        this.ExceptionHandler(Catalog.GetString("Read spectrum is not valid."), null);
                        return null;
                    }

                    this.SpectrumCount = 1;

                    WavelengthLimits limits = new WavelengthLimits(lut.MinWavelength, lut.MaxWavelength);
                    spectrum = new Spectrum(new DataSet(lambda, intensity), limits)
                    {
                        AddData = add_data
                    };

                    if (this.is_finder && auto_finder_light)
                        this.SetFinderLightSource(false, idle_task);
                }

                DateTime now = DateTime.Now;

                spectrum.Serial             = this.info.SerialNumber;
                spectrum.FirmwareVersion    = this.FirmwareVersion;
                spectrum.LutTimestamp       = this.LutTimestamp.ToString();
                spectrum.SpectralResolution = this.HardwareProperties.SpectralResolution;
                spectrum.AverageCount       = this.CurrentConfig.Average;
                spectrum.SpectrumType       = SpectrumType.Single;

                if (this.IsFinder)
                    spectrum.ProbeType = ProbeType.FinderStandard;

                spectrum.Timestamp      = now;
                spectrum.Id             = "1_" + now.ToString("yyyy-MM-dd_HH-mm-ss_") + this.info.SerialNumber;
                spectrum.Label          = now.ToString() + " " + this.CurrentConfig.Name;
                spectrum.DefaultLabel   = true;
                spectrum.Comment        = this.CurrentConfig.DefaultComment;
                spectrum.IsFromFile     = false;
                spectrum.Number         = (long)this.SpectrumCount;
                spectrum.LimitBandwidth = this.LimitBandwith;

                spectrum.HardwareInfo = this.FinderVersion.ToArray();

                if (this.CurrentConfig.HasAdcCorrection)
                    spectrum.AdcCorrection = this.CurrentConfig.AdcCorrection;

                if (this.StateNotification)
                this.NewSingleNotify(spectrum.Clone());

                return spectrum;
            }
            catch (Exception ex)
            {
                if ((ex is InvalidSpectrumException) && (this.DisableInternalExceptionHandling || this.IgnoreSgsExceptions))
                    this.SetState(Spectrometer.State.Idle);

                this.ExceptionHandler(Catalog.GetString("Cannot read single spectrum:") + " " + ex.Message, ex);
                return null;
            }
        }

        public override void StartQuickStream()
        {
            Thread run_quick = new Thread(new ThreadStart(this.DoRunQuick));
            run_quick.Start();
        }

        public override void StartStream()
        {
            this.quick_stream_running = false;

            switch (this.CurrentConfig.StreamType)
            {

            case Spectrometer.StreamType.Continuous:
                Thread run_continuous = new Thread(new ThreadStart(this.DoRunContinuous));
                run_continuous.Start();
                break;

            case Spectrometer.StreamType.Timed:
                Thread run_timed = new Thread(new ThreadStart(this.DoRunTimed));
                run_timed.Start();
                break;

            default:
                throw new Exception("Unknown StreamType");

            }
        }

        public override void StopStream()
        {
            if (this.quick_stream_running)
            {
                this.SetState(Spectrometer.State.IdleRequest);
                return;
            }

            switch (this.CurrentConfig.StreamType)
            {

            case Spectrometer.StreamType.Continuous:
                throw new NotImplementedException();

            case Spectrometer.StreamType.Timed:
                this.SetState(Spectrometer.State.IdleRequest);
                break;

            default:
                throw new Exception("Unknown StreamType");

            }
        }

        public override void WriteLut(byte[] key, Spectrometer.LUT lut)
        {
            this.WriteLookupTable(key, new LutWriteData(lut));
            this.lookup_tables[(Protocol.ChannelType)lut.AcquisitionChannelMask] = lut;
        }

        public override double ReadStatus()
        {
            return 0.0;
        }

        public override void SetFinderLightSource(bool state, IdleTaskHandler idle_task)
        {
            Protocol.Command command = new Protocol.Command(Protocol.PeripheralSetLightSource)
            {
                ByteValue = state ? (byte)1 : (byte)0
            };

            this.SetState(Spectrometer.State.Waiting);
            Protocol.Response response = this.WriteCommand(command, 5000);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot set light source."), this);

            if (response.ByteValue != 0)
                throw new SgsException(Catalog.GetString("Cannot set light source: Peripheral returned error."), this);

            this.FinderLightSourceState = state;

            lock (this)
                this.state_changed = true;

            idle_task?.Invoke();

            if (state == true)
                Thread.Sleep((int)this.LightSourceDelay);

            this.FinderLightSourceState = state;

            this.SetState(Spectrometer.State.Idle);
        }

        public override void SetFinderWheel(FinderWheelPosition position, IdleTaskHandler idle_task, bool no_wait = false)
        {
            Protocol.Command command = new Protocol.Command(Protocol.PeripheralSetWheelPosition)
            {
                ByteValue = (byte)position
            };

            this.FinderWheelPosition = FinderWheelPosition.Moving;
            lock (this)
                this.state_changed = true;

            Protocol.Response response = this.WriteCommand(command, 5000);

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot set Reference Wheel."), this);

            if (response.ByteValue != 0)
                throw new SgsException(Catalog.GetString("Cannot set Reference Wheel: Peripheral returned error."), this);

            this.FinderWheelPosition = position;
            idle_task?.Invoke();
        }

        public override void SetFinderFanSpeed(bool on)
        {
            throw new NotSupportedException("Not supported by hardware.");
        }

        public override byte ReadDimmerParameter()
        {
            throw new NotSupportedException("Not supported by hardware.");
        }

        public override bool FinderButtonPressed()
        {
            return false;
        }

        public override double ReadFinderTemperature()
        {
            throw new NotSupportedException("Not supported by hardware.");
        }

        public override byte[] ReadFinderHardwareVersion()
        {
            throw new NotSupportedException("Not supported by hardware.");
        }

        public override int ReadAcquisitionOffset()
        {
            return 0;
        }

        private void DoRunSingle(object o)
        {
            bool auto_finder_light = (bool)o;

            try
            {
                Spectrum spectrum = this.Single(auto_finder_light);
                this.spectrum_queue.Enqueue(spectrum);
            }
            catch (System.Exception ex)
            {
                this.ExceptionHandler(Catalog.GetString("Cannot read single spectrum:") + " " + ex.Message, ex);
            }
        }

        private void DoRunTimed()
        {
            try
            {
                if (this.GetState() == Spectrometer.State.QuickStream)
                {
                    this.StopStream();
                    this.WaitForIdle(5 * this.CurrentConfig.Timeout);
                    this.StartStream();
                    return;
                }

                this.SetFinderLightSource(true, null);

                if (this.SetState(Spectrometer.State.Stream) == false)
                    throw new Exception(Catalog.GetString("Device is not ready."));

                DateTime then = DateTime.Now;
                this.SpectrumCount = 0;

                while ((this.GetState() == Spectrometer.State.Stream) &&
                       ((this.current_config.TimedCount == 0) || (this.SpectrumCount < this.CurrentConfig.TimedCount)))
                {
                    try
                    {
                        this.ReadSpectrum(out double[] lambda, out double[] intensity, out uint[] add_data, out Spectrometer.LUT lut, null);

                        WavelengthLimits limits = new WavelengthLimits(lut.MinWavelength, lut.MaxWavelength);
                        Spectrum spectrum = new Spectrum(new DataSet(lambda, intensity), limits)
                        {
                            Timestamp          = DateTime.Now,
                            Serial             = this.Info.SerialNumber,
                            FirmwareVersion    = this.FirmwareVersion,
                            LutTimestamp       = this.LutTimestamp.ToString(),
                            SpectralResolution = this.HardwareProperties.SpectralResolution,
                            AverageCount       = this.CurrentConfig.Average,
                            SpectrumType       = SpectrumType.Stream,
                            AddData            = add_data
                        };

                        if (this.IsFinder)
                            spectrum.ProbeType = ProbeType.FinderStandard;

                        spectrum.Id             = "5_stream" + this.info.SerialNumber;
                        spectrum.Label          = Enum.GetName(typeof(Spectrometer.State), Spectrometer.State.Stream) + " " + this.CurrentConfig.Name;
                        spectrum.DefaultLabel   = true;
                        spectrum.Comment        = this.CurrentConfig.DefaultComment;
                        spectrum.IsFromFile     = false;
                        spectrum.Number         = (long)(++this.SpectrumCount);
                        spectrum.LimitBandwidth = this.LimitBandwith;

                        spectrum.HardwareInfo = this.FinderVersion.ToArray();

                        if (this.CurrentConfig.HasAdcCorrection)
                            spectrum.AdcCorrection = this.CurrentConfig.AdcCorrection;

                        if (this.StateNotification)
                            this.NewStreamNotify(spectrum.Clone());

                        this.spectrum_queue.Enqueue(spectrum);

                        lock (this.current_spectrum_mutex)
                        this.current_spectrum = spectrum;
                    }
                    catch (Exception ex)
                    {
                        if ((ex is InvalidSpectrumException) && (this.DisableInternalExceptionHandling || this.IgnoreSgsExceptions))
                            this.SetState(Spectrometer.State.Idle);

                        this.ExceptionHandler(Catalog.GetString("Cannot read in timed mode:") + " " + ex.Message, ex);
                        break;
                    }

                    TimeSpan span;
                    ulong mseconds = 0;
                    ulong maxtime = 0;
                    bool light_is_on = true;

                    while (this.GetState() == Spectrometer.State.Stream)
                    {
                        if (this.CurrentConfig.TimedSpan == 0)
                            break;

                        span = DateTime.Now - then;
                        mseconds = (ulong)span.TotalMilliseconds % (this.CurrentConfig.TimedSpan * 1000);

                        if (mseconds < maxtime)
                            break;
                        else
                            maxtime = mseconds;

                        if ((this.CurrentConfig.TimedCount != 0) && (this.SpectrumCount >= this.CurrentConfig.TimedCount))
                            break;

                        // switch finder light off if there are more than 3.5 seconds to wait
                        if (this.IsFinder && light_is_on && this.CurrentConfig.TimedSpan * 1000 - mseconds > 3500)
                        {
                            Console.WriteLine("Next acquisition in {0} ms.",
                                              this.CurrentConfig.TimedSpan * 1000 - mseconds);
                            lock (this)
                            {
                                this.SetState(Spectrometer.State.Idle);
                                this.SetFinderLightSource(false, null);
                                this.SetState(Spectrometer.State.Stream);
                            }
                            light_is_on = false;
                        }

                        Thread.Sleep(50);
                    }

                    if (light_is_on == false && 
                        this.GetState() == Spectrometer.State.Stream &&
                        (this.SpectrumCount < this.CurrentConfig.TimedCount || this.CurrentConfig.TimedCount == 0))
                    {
                        lock (this)
                        {
                            this.SetState(Spectrometer.State.Idle);
                            this.SetFinderLightSource(true, null);
                            this.SetState(Spectrometer.State.Stream);
                        }
                    }
                }
                Console.WriteLine("Queue contains still " + this.spectrum_queue.Count + " spectra.");

                Spectrometer.State state = this.GetState();

                if (state == Spectrometer.State.Unknown)
                    return;

                if (state != Spectrometer.State.Disconnected)
                    this.SetState(Spectrometer.State.Idle);

                this.SetFinderLightSource(false, null);
            }
            catch (Exception ex)
            {
                this.ExceptionHandler(Catalog.GetString("Cannot read in timed mode:") +
                                      " " + ex.Message, ex);
            }
        }

        private void DoRunQuick()
        {
            try
            {
                if (this.GetState() == Spectrometer.State.Continuous ||
                    this.GetState() == Spectrometer.State.Stream)
                {
                    this.StopStream();
                    this.WaitForIdle(this.CurrentConfig.Timeout);

                    this.StartQuickStream();
                    return;
                }

                Console.WriteLine("Write QuickStream config");
                this.old_config = new Spectrometer.Config(this.CurrentConfig);
                this.CurrentConfig.Average = this.CurrentConfig.QuickAverage;
                this.CurrentConfig.Samples = this.CurrentConfig.QuickSamples;
                this.quick_stream_running = true;
                this.WriteConfig(this.CurrentConfig);

                this.SetFinderLightSource(true, null);

                if (this.SetState(Spectrometer.State.QuickStream) == false)
                    throw new Exception(Catalog.GetString("Device is not ready."));

                while (this.GetState() == Spectrometer.State.QuickStream)
                {
                    try
                    {
                        this.ReadSpectrum(out double[] lambda, out double[] intensity, out uint[] add_data, out Spectrometer.LUT lut, null);

                        WavelengthLimits limits = new WavelengthLimits(lut.MinWavelength, lut.MaxWavelength);
                        Spectrum spectrum = new Spectrum(new DataSet(lambda, intensity), limits)
                        {
                            Timestamp          = DateTime.Now,
                            Serial             = this.Info.SerialNumber,
                            FirmwareVersion    = this.FirmwareVersion,
                            LutTimestamp       = this.LutTimestamp.ToString(),
                            SpectralResolution = this.HardwareProperties.SpectralResolution,
                            AverageCount       = this.CurrentConfig.Average,
                            SpectrumType       = SpectrumType.QuickStream,
                            Id                 = "5_stream" + this.info.SerialNumber,
                            Label              = Enum.GetName(typeof(Spectrometer.State), Spectrometer.State.QuickStream) + " " + this.CurrentConfig.Name,
                            DefaultLabel       = true,
                            Comment            = this.CurrentConfig.DefaultComment,
                            IsFromFile         = false,
                            LimitBandwidth     = this.LimitBandwith,
                            HardwareInfo       = this.FinderVersion.ToArray(),
                            AddData            = add_data
                        };

                        if (this.CurrentConfig.HasAdcCorrection)
                            spectrum.AdcCorrection = this.CurrentConfig.AdcCorrection;

                        if (this.StateNotification)
                            this.NewQuickStreamNotify(spectrum.Clone());

                        this.spectrum_queue.Enqueue(spectrum);

                        lock (this.current_spectrum_mutex)
                        this.current_spectrum = spectrum;

                    }
                    catch (Exception ex)
                    {
                        if ((ex is InvalidSpectrumException) && (this.DisableInternalExceptionHandling || this.IgnoreSgsExceptions))
                            this.SetState(Spectrometer.State.Idle);

                        this.ExceptionHandler(Catalog.GetString("Cannot read in quick stream mode:") + " " + ex.Message, ex);
                        break;
                    }
                }

                Console.WriteLine("Queue contains still " + this.spectrum_queue.Count + " spectra.");

                Spectrometer.State state = this.GetState();

                if (state == Spectrometer.State.Unknown)
                    return;

                if (state != Spectrometer.State.Disconnected)
                    this.SetState(Spectrometer.State.Idle);

                this.SetFinderLightSource(false, null);

                this.CurrentConfig = this.old_config;
                this.old_config = null;
                this.WriteConfig(this.CurrentConfig);
            }
            catch (Exception ex)
            {
                this.ExceptionHandler(Catalog.GetString("Cannot read in quick stream mode:") +
                                      " " + ex.ToString(), ex);
            }
        }

        private void DoRunContinuous()
        {
            throw new NotImplementedException();
        }

        internal Protocol.Response WriteCommand(Protocol.Command cmd, int timeout, IdleTaskHandler idle_task = null, bool throw_on_error = true)
        {
            try
            {
                Spectrometer.State sav_state = this.GetState();

                cmd.UpdateChecksum();

                Console.WriteLine("Writing {0}", cmd);
                uint result = this.Write(cmd.Buffer);

                this.SetState(sav_state);

                if (result != cmd.Buffer.Length)
                    throw new SgsException(Catalog.GetString("Transmission interrupted."), this);

                Thread.Sleep(10);

                return this.ReadResponse(timeout, cmd, idle_task, throw_on_error);
            }
            catch (Exception ex)
            {
                this.ExceptionHandler(Catalog.GetString("Cannot transmit parameter:") + " " + ex.Message, ex);
                return null;
            }
        }

        internal Protocol.Response ReadResponse(int timeout, Protocol.Command cmd, IdleTaskHandler idle_task, bool throw_on_error = true)
        {
            Spectrometer.State sav_state = this.GetState();

            byte[] header = this.ReadBlocking(BaseProtocol.HEADER_LENGTH +
                                              BaseProtocol.PAYLOAD_CMD_LENGTH + 
                                              BaseProtocol.PAYLOAD_LEN_LENGTH, 
                                              timeout, 
                                              idle_task);

            UInt16 payload_data_len = BitConverter.ToUInt16(header, (int)BaseProtocol.PAYLOAD_LEN_OFFSET);

            if (payload_data_len > BaseProtocol.PAYLOAD_MAX_LENGTH)
                throw new Exception(string.Format(Catalog.GetString("Payload length exceeds transfer limit: {0} > {1}"), 
                                                  payload_data_len, 
                                                  BaseProtocol.PAYLOAD_MAX_LENGTH));

            int count = BaseProtocol.HEADER_LENGTH +
                        BaseProtocol.PAYLOAD_CMD_LENGTH + 
                        BaseProtocol.PAYLOAD_LEN_LENGTH +
                        payload_data_len +
                        BaseProtocol.CHECKSUM_LENGTH;

            int try_count = 0;

            while (true)
            {
                try
                {
                    byte[] buf = this.ReadBlocking(count, timeout, idle_task);
                    Protocol.Response response = new BaseProtocol.Response(buf, cmd.ResponseDataType);
                    Console.WriteLine("Reading {0}", response);

                    BaseProtocol.Errno errno = response.Check(cmd);

                    if (throw_on_error && errno != BaseProtocol.Errno.Success)
                        throw new SgsException(string.Format(Catalog.GetString("Device returned Error {0} ({1})."), errno, (int)errno), this);

                    this.SetState(sav_state);

                    return response;
                }
                catch (ChecksumException ex)
                {
                    if (++try_count > BaseProtocol.INVALID_CHECKSUM_RETRY_COUNT)
                        throw;

                    Console.WriteLine(string.Format(Catalog.GetString("Cannot read device response (try again): {0}"), ex.Message));
                }
            }
        }

        public override FinderWheelPosition FinderWheelPosition { get; protected set; }
        public override bool FinderLightSourceState { get; protected set; }

        public override int LutLength => Prototype.LUT_LENGTH;
    }
}

