// Created with MonoDevelop
//
//    Copyright (C) 2020 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.Diagnostics;
using System.Linq;
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.Sgs1900;
using Hiperscan.SGS.SgsNt.Protocol;
using Hiperscan.Spectroscopy;
using Hiperscan.Unix;

using Version = Hiperscan.SGS.Common.Version;


namespace Hiperscan.SGS.SgsNt
{
    internal partial class DeviceNt1_1 : Sgs1900.BaseDevice
    {
        private const double AVERAGING_WINDOW_SIZE = 5.0;

        public override int LutLength => DeviceNt1_1.LUT_LENGTH;

        private int oscillation_mode = 0;
        private ushort adc_single_samples = 160;
        private bool use_raw_wavelengths = false;

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

            try
            {
                this.serial_interface = new STM32(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.Unknown);

                        if (auto_init == false)
                            return;

                        this.scan_engine_msa_factor = this.ReadScanEngineMsaFactor();

                        try
                        {
                            this.use_raw_wavelengths = Environment.GetEnvironmentVariable(Env.HCL.SGSNT_USE_RAW_WAVELENGTHS) == "1";
                            this.oscillation_mode = 0;
                            this.active_channel = this.GetDefaultChannel();

                            if (this.lookup_tables.ContainsKey(this.active_channel) == false)
                            {
                                Spectrometer.LUT lut = this.ReadLookupTable(this.active_channel);
                                this.lookup_tables.Add(this.active_channel, lut);

                                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.SetState(Spectrometer.State.Idle);

                        }
                        catch (SgsException ex)
                        {
                            Console.WriteLine("Exception while reading LUT: " + ex.ToString());
                            this.SetState(Spectrometer.State.Unprogrammed);
                        }

                        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();
                        }
                        try
                        {
                            this.current_config = this.ReadConfig();

                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("Exception while reading Config: " + ex.ToString());
                            this.SetState(Spectrometer.State.Unprogrammed);
                        }

                        FirmwareInfoReadData firmware_info = this.ReadFirmwareInfo();

                        this.firmware_version = $"{firmware_info.Major}.{firmware_info.Minor}.{firmware_info.Patch}";
                        this.firmware_revision = $"{firmware_info.Build}";

                        if (this.GetState() == Spectrometer.State.Idle)
                            this.lut_timestamp = this.lookup_tables.Values.First().Timestamp;

                        //this.clock_frequency = this.ReadClockFrequency();
                        PeripheralInfoReadData? peripheral_info = null;
                        if (this.GetState() != Spectrometer.State.Unprogrammed)
                        {
                            peripheral_info = this.ReadPeripheralInfo();
                            if (peripheral_info != null)
                            {
                                this.is_finder = true;
                                this.FinderVersion = new Benchtop.FinderSetup.FinderVersion(((PeripheralInfoReadData)peripheral_info).VersionBytes);
                                this.SetFinderWheel(FinderWheelPosition.Initial, null);
                                this.SetFinderLightSource(false, null);
                            }
                            else
                            {
                                this.is_finder = false;
                            }
             
                            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));
        }

        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 void SetOscillationMode(byte[] key, ushort factor)
        {
            this.SetMaintenanceMode(key);

            this.oscillation_mode = factor;

            Protocol.Command command = new Protocol.Command(Protocol.OscillationMode)
            {
                UInt16Value = factor
            };

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

        public override void SetTestMode(byte[] key, byte mode, ushort param)
        {
            this.SetMaintenanceMode(key);

            byte[] payload_buf = { mode };
            payload_buf = payload_buf.Concat(BitConverter.GetBytes((UInt16)(param))).ToArray();

            Protocol.Command command = new Protocol.Command(Protocol.TestMode)
            {
                ByteArrayValue = payload_buf
            };

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

        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, TIMOUT_MSA_MS);

            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, TIMOUT_COMMAND_MS, idle_task);

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

        public override void Close()
        {
            if (this.GetState() != Spectrometer.State.Disconnected)
                this.Disconnect();

            lock (this.opening_mutex)
            {
                if ((this.GetState() == Spectrometer.State.Continuous) ||
                    (this.GetState() == Spectrometer.State.Stream) ||
                    (this.GetState() == Spectrometer.State.QuickStream))
                    this.StopStream();

#pragma warning disable RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist
                if (this.GetState() != Spectrometer.State.Disconnected)
                {
                    try
                    {
                        if (this.IsShared)
                            this.Unshare();
                    }
                    catch { }

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

                try
                {
                    this.serial_interface.Close();
                }
                catch { }
#pragma warning restore RECS0022 // catch-Klausel, die System.Exception abfängt und keinen Text aufweist
            }
        }

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

            this.ReadHardwareProperties();
        }

        protected override void Disconnect()
        {
            try
            {
                this.IgnoreSgsExceptions = true;
                this.NotifyBeforeDisconnect();
                this.WriteCommand(Protocol.Disconnect, TIMOUT_COMMAND_MS);

            }
            catch (Exception ex)
            {
                Console.WriteLine("Cannot disconnect device: " + ex.Message);
            }
        }

        internal Protocol.ChannelType AcquireSpectrum(Spectrometer.Config config, Protocol.ChannelType channel, IdleTaskHandler idle_task = null)
        {
            Protocol.Command command = new Protocol.Command(Protocol.AcquireSpectrum);

            byte[] payload_buf = BitConverter.GetBytes((UInt16)config.Average);
            payload_buf = payload_buf.Concat(BitConverter.GetBytes((UInt16)(config.Timeout - TRANSFER_TIMEOUT_OVERHEAD))).ToArray();
            payload_buf = payload_buf.Concat(new byte[] { (byte)channel }).ToArray();
            payload_buf = payload_buf.Concat(new byte[] { (byte)config.TriggerMode }).ToArray();

            command.ByteArrayValue = payload_buf;

            int timeout = this.CurrentConfig.Timeout;
            if (this.oscillation_mode > 0) timeout *= this.oscillation_mode;

            Protocol.Response response = this.WriteCommand(command,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, TIMOUT_COMMAND_MS, 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)
        {
            this.serial_interface.Purge();

            Spectrometer.Config config = new Spectrometer.Config(this)
            {
                AutoTimeout = this.CurrentConfig.AutoTimeout,
                Timeout = this.CurrentConfig.Timeout,

                Name = this.ReadDeviceName(),
            };

            if (this.lookup_tables.Count == 0)
                config.LambdaCenter = 65535;

            if (config.IsValid == false && throw_if_invalid)
                throw new SgsException(Catalog.GetString("Device configuration is not valid."), this);

            config.IsInitialized = true;
            return config;
        }

        internal override void WriteConfig(Spectrometer.Config config)
        {
            this.SetState(Spectrometer.State.Idle);

            //this.WriteSampleCount(config.Samples);
            //this.WriteAverageCount(config.Average);
            this.WriteDeviceName(config.Name);
        }

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

        public override void SetAdcCount(byte[] key, ushort samples)
        {
            this.adc_single_samples = samples;
        }

        public override ushort[] ReadAdcValues(byte[] key)
        {
            this.SetMaintenanceMode(key);

            Protocol.Command command = new Protocol.Command(Protocol.ReadAdcValues);

            byte[] payload_buf = { (byte)BaseProtocol.ChannelType.Channel1 };
            payload_buf = payload_buf.Concat(BitConverter.GetBytes((UInt16)(adc_single_samples))).ToArray();
            command.ByteArrayValue = payload_buf;

            return this.WriteCommand(command, TIMOUT_COMMAND_MS).UInt16ArrayValue;
        }

        private Protocol.ChannelType GetDefaultChannel()
        {
            string default_channel = Environment.GetEnvironmentVariable(Env.HCL.SGSNT_DEFAULT_ACQUISITION_CHANNEL) ?? "0";
            Protocol.ChannelType channel = BaseProtocol.ChannelType.Channel1;

            int channel_index = int.Parse(default_channel);
            if (channel_index < ALL_CHANNELS.Length)
            {
                channel = ALL_CHANNELS[channel_index];
            }

            if (this.ACTIVE_CHANNELS.Contains(channel) == false)
            {
                throw new Exception(string.Format(Catalog.GetString("Channel '{0}' not supported by hardware."), channel));
            }

            return channel;
        }

        private void ConvertSpectrumLambda(IntensityReadData read_data, Spectrometer.LUT _lut, out double[] lambdas, out double[] intensities)
        {
            DataSet raw_intensity = new DataSet(this.lookup_tables[this.active_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;
            }
        }

        private void ConvertSpectrumLambdaRaw(IntensityReadData read_data, Spectrometer.LUT _lut, out double[] lambdas, out double[] intensities)
        {
            DataSet raw_intensity = new DataSet(this.lookup_tables[this.active_channel].Data.Take(read_data.Intensities.Length),
                                    read_data.Intensities);
            var raw_intensity_limit = raw_intensity.AsEnumerable()
                                         .Where(v => v.Item1 >= _lut.MinWavelength - _lut.ExtensionRange && v.Item1 <= _lut.MaxWavelength + _lut.ExtensionRange)
                                         .ToList();

            lambdas = raw_intensity_limit.Select(l => l.Item1).ToArray();
            intensities = raw_intensity_limit.Select(l => l.Item2 * INTENSITY_SCALE_FACTOR).ToArray();
        }

        private void ConvertSpectrumIndex(IntensityReadData read_data, out double[] lambdas, out double[] intensities)
        {
            lambdas = Enumerable.Range(0, read_data.Intensities.Length).Select(x => (double)x).ToArray();
            intensities = read_data.Intensities.Select(d => (double)d * INTENSITY_SCALE_FACTOR).ToArray();
        }

        private void ReadSpectrum(out double[] lambdas, out double[] intensities, out uint[] add_data, out Spectrometer.LUT lut, out ushort average, IdleTaskHandler idle_task)
        {
            if (this.lookup_tables.ContainsKey(this.active_channel) == false)
                throw new KeyNotFoundException(string.Format(Catalog.GetString("Cannot find Lookup Table for acquisition channel {0}."),
                                                             this.active_channel));

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

            this.AcquireSpectrum(this.CurrentConfig, this.active_channel, idle_task);
             IntensityReadData read_data = this.ReadIntensity(this.active_channel, idle_task);

            if(this.oscillation_mode == 0)
            {
                if(use_raw_wavelengths)
                    ConvertSpectrumLambdaRaw(read_data, _lut, out lambdas, out intensities);
                else
                    ConvertSpectrumLambda(read_data, _lut, out lambdas, out intensities);
            }
            else { 
             ConvertSpectrumIndex(read_data, out lambdas, out intensities);
            }

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

            double mirror_freq = (double)read_data.MemsFrequency;
            int samples = LUT_LENGTH;
            double sample_rate = (double)samples * mirror_freq * 2f;

            lock (this)
            {
                this.LastTemperature = read_data.Temperature;
                this.acquisition_info = new AcquisitionInfo(mirror_freq, samples, sample_rate);

                if (!double.IsInfinity(mirror_freq) && !double.IsNaN(mirror_freq))
                {
                    TimeSpan span = DateTime.Now - this.creation_time;
                    this.frequency.Add((double)span.TotalSeconds, mirror_freq);
                }
                this.state_changed = true;
            }
        }

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

        public override Spectrum Single(bool auto_finder_light, IdleTaskHandler idle_task)
        {
            try
            {
                Stopwatch sw = new Stopwatch();
                Spectrum spectrum = null;
                ushort average = this.CurrentConfig.Average;

                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."));

                    sw.Start();
                    this.ReadSpectrum(out double[] lambda,
                                      out double[] intensity,
                                      out uint[] add_data,
                                      out Spectrometer.LUT lut,
                                      out average,
                                      idle_task);
                    sw.Stop();
                    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 = average;    // set to effective sampled average cycles reported by hardware
                spectrum.SpectrumType = SpectrumType.Single;

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

                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 (Environment.GetEnvironmentVariable(Env.HCL.LOG_ACQUISITION_PERIOD) == "1")
                    spectrum.Comment += "Spectrum acquisition period: " + sw.Elapsed.ToString();

                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)
        {
            lut.AcquisitionChannelMask = (byte)this.active_channel;
            this.WriteLookupTable(key, new LutWriteData(lut));
            this.lookup_tables[(Protocol.ChannelType)lut.AcquisitionChannelMask] = lut;
        }

        public override void WriteSerialNumber(byte[] key, string serial)
        {
            Console.WriteLine("Saving serial number \"{0}\".", serial);
            this.SetMaintenanceMode(key);
            if (WriteStringParameter(Protocol.WriteSerialNumber, serial) == null)
                throw new SgsException(Catalog.GetString("Cannot write serial number."), this);
        }

        public override string ReadSerialNumber()
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadSerialNumber);

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

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot write serial number."), this);

            return response.StringValue;
        }

        public override byte[] ReadStepperMotorParameters()
        {
            return new byte[8];
        }

        public override void WriteHardwareVersion(byte[] key, byte major, byte minor)
        {
            this.SetMaintenanceMode(key);

            Protocol.Command command = new Protocol.Command(Protocol.WriteHardwareVersion);

            byte[] buf = new byte[command.PayloadLength];
            buf[0] = major;
            buf[1] = minor;

            command.ByteArrayValue = buf;

            if (this.WriteCommand(command, TIMOUT_COMMAND_MS) == null)
                throw new SgsException(Catalog.GetString("Cannot write hardware version."), this);
        }

        public override void WriteTemperatureControlInfo(byte[] key, byte version, byte control_value, double target_temperature)
        {
            if (this.IsFinder == false)
                throw new NotSupportedException(Catalog.GetString("Not supported by hardware."));

            this.SetMaintenanceMode(key);

            var data = new TcrtlInfoData(version, control_value, target_temperature);
            Protocol.Command command = new Protocol.Command(Protocol.WriteTemperatureControlInfo)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(data)
            };

            if (this.WriteCommand(command, TIMOUT_COMMAND_MS) == null)
                throw new SgsException(Catalog.GetString("Cannot write temperature control info."), this);
        }

        public override void ReadTemperatureControlInfo(out byte version, out byte control_value, out double target_temperature)
        {
            if (!this.IsFinder)
                throw new NotSupportedException(Catalog.GetString("Not supported by hardware."));

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

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

            var tctrl_info = (TcrtlInfoData)Protocol.TransferStruct.FromBuffer(response.TransferStructValue, typeof(TcrtlInfoData));
            version = tctrl_info.Version;
            control_value = tctrl_info.ControlValue;
            target_temperature = (double)tctrl_info.TargetTemperature / 100.0;
        }

        public override string ReadSystemLog(byte[] key)
        {
            this.SetMaintenanceMode(key);

            Protocol.Command command = new Protocol.Command(Protocol.ReadSystemLog);
            Protocol.Response response = this.WriteCommand(command, TIMOUT_LONGCOMMAND_MS);
            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot read spectrometer system log."), this);

            return response.StringValue;
        }

        public override void WriteUsbProductId(byte[] key, UInt16 pid)
        {
            this.SetMaintenanceMode(key);
            Protocol.Command command = new Protocol.Command(Protocol.WriteUsbPid)
            {
                UInt16Value = pid
            };

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

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

        public override void WriteHardwareProperties(byte[] key, HardwareProperties device_properties)
        {
            this.SetMaintenanceMode(key);
            HwPropertyData data = new HwPropertyData(device_properties);
            Protocol.Command command = new Protocol.Command(Protocol.WriteHardwareProperties)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(data)
            };

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

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

        internal void ReadHardwareProperties()
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadHardwareProperties);

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

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

            byte[] buf = response.PayloadData;

            if (buf.Length == 1)
            {
                if (buf[0] == 0)
                {
                    this.NotifyWarning(Catalog.GetString("Hardware properties are invalid or not set."));
                    this.HardwareProperties.SetInvalid();
                    return;
                }
                else
                {
                    throw new SgsException(Catalog.GetString("Invalid device response."), this);
                }
            }

            byte type_byte1 = buf[0];
            byte type_byte2 = buf[1];
            if (type_byte1 != byte.MaxValue)
                this.HardwareProperties.Type = string.Format("{0}{1}", (char)type_byte1, (char)type_byte2);

            this.HardwareProperties.GridConstant = BitConverter.ToUInt16(buf, 2);
            this.HardwareProperties.IncidenceAngle = BitConverter.ToUInt16(buf, 4) / 10.0;
            this.HardwareProperties.DiffractionAngle = BitConverter.ToUInt16(buf, 6) / 10.0;
            this.HardwareProperties.MinTemperature = BitConverter.ToInt16(buf, 8) / 100.0;
            this.HardwareProperties.MaxTemperature = BitConverter.ToInt16(buf, 10) / 100.0;
            this.HardwareProperties.SpectralResolution = BitConverter.ToUInt16(buf, 12) / 10.0;
            this.HardwareProperties.WavelengthDrift = BitConverter.ToUInt16(buf, 14) / 10.0;
            this.HardwareProperties.Sensitivity = BitConverter.ToUInt32(buf, 16);
            this.HardwareProperties.SignalNoiseRatio = BitConverter.ToUInt16(buf, 20);
            this.HardwareProperties.DynamicRange = BitConverter.ToUInt16(buf, 22);
            this.HardwareProperties.MinWavelength = BitConverter.ToUInt16(buf, 24) / 10.0;
            this.HardwareProperties.MaxWavelength = BitConverter.ToUInt16(buf, 26) / 10.0;
            this.HardwareProperties.WavelengthIncrement = BitConverter.ToUInt16(buf, 28) / 10.0;
            this.HardwareProperties.WavelengthExtension = BitConverter.ToUInt16(buf, 30) / 10.0;
            this.HardwareProperties.MinAverage = BitConverter.ToUInt16(buf, 32);
            this.HardwareProperties.MaxAverage = BitConverter.ToUInt16(buf, 34);

            if (buf.Length >= 38)
                this.HardwareProperties.MEMSInfo = BitConverter.ToUInt16(buf, 36);

            if (buf.Length >= 40)
                this.HardwareProperties.AdcConfigurationId = BitConverter.ToUInt16(buf, 38);
        }

        public override void WriteMasterKey(byte[] maintenance_key, byte[] key)
        {
            this.SetMaintenanceMode(maintenance_key);

            Protocol.Command command = new Protocol.Command(Protocol.WriteKey)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(new KeyData(KeyIndex.General, key))
            };

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

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

        public override byte[] ReadMasterKey(byte[] challenge)
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadKey)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(new ChallengeData(KeyIndex.General, challenge))
            };

            return ReadByteArrayParameter(command, TIMOUT_COMMAND_MS);
        }

        public override void WriteIndividualKey(byte[] maintenance_key, byte[] key)
        {
            this.SetMaintenanceMode(maintenance_key);

            Protocol.Command command = new Protocol.Command(Protocol.WriteKey)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(new KeyData(KeyIndex.Individual, key))

            };

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

            if (response == null)
                throw new SgsException(Catalog.GetString("Cannot write individual key."), this);

        }

        public override byte[] ReadIndividualKey(byte[] challenge)
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadKey)
            {
                TransferStructValue = Protocol.TransferStruct.GetBytes(new ChallengeData(KeyIndex.Individual, challenge))
            };

            return ReadByteArrayParameter(command, TIMOUT_COMMAND_MS);
        }

        internal bool ReadKeyIsProgrammed()
        {
            if (this.key_is_programmed == false)
            {
                key_is_programmed = (ReadByteParameter(Protocol.KeyIsProgrammed, TIMOUT_COMMAND_MS) == 0);
            }
            return this.key_is_programmed;
        }

        public override bool HasKeyPad
        {
            get { return this.ReadKeyIsProgrammed(); }
        }

        public override double ReadStatus()
        {
            Protocol.Response response = this.WriteCommand(new Protocol.Command(Protocol.ReadStatus), TIMOUT_COMMAND_MS);

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

            StatusReadData stat = (StatusReadData)Protocol.TransferStruct.FromBuffer(response.TransferStructValue, typeof(StatusReadData));

            this.sgs_firmware_status = (SGSFirmwareStatusType)stat.ErrorFlag;

            return this.LastTemperature = (double)stat.Temperature / 100.0;
        }

        public override void WriteOemSerialNumber(string serial)
        {
            if (WriteStringParameter(Protocol.WriteOemSerialNumber, serial) == null)
                throw new SgsException(Catalog.GetString("Cannot write oem serial."), this);
        }

        public override string ReadOemSerialNumber()
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadOemSerialNumber);
            Protocol.Response response = this.WriteCommand(command, this.current_config.Timeout);

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

            return response.StringValue.Trim();
        }

        internal override void WriteDeviceName(string name)
        {
            Console.WriteLine("Saving device name \"{0}\".", name);
            if (WriteStringParameter(Protocol.WriteDeviceName, name) == null)
                throw new SgsException(Catalog.GetString("Cannot write device name."), this);
        }

        internal override string ReadDeviceName()
        {
            Protocol.Command command = new Protocol.Command(Protocol.ReadDeviceName);
            Protocol.Response response = this.WriteCommand(command, this.current_config.Timeout);

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

            string name = response.StringValue;
            if (string.IsNullOrEmpty(name))
                return name;
            Console.WriteLine("Read device name: \"{0}\"", name);
            if (name.Length == name.Trim().Length)
                return this.Info.SerialNumber;
            else
                return name.Trim();
        }

        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, out ushort average, 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 = average,
                            SpectrumType = SpectrumType.Stream,
                            AddData = add_data
                        };

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

                        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, out ushort average, 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 = 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();
        }

        public override void RebootToBootloader()
        {
            var buf = new byte[] { CMD_REBOOT_BOOTLOADER };
            this.Write(buf);
            return;
        }
    }
}

