// 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.Linq;
using System.Runtime.InteropServices;
using System.Threading;

using Hiperscan.SGS.Common;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.SGS.SgsNt.Protocol;
using Hiperscan.Unix;


namespace Hiperscan.SGS.SgsNt
{
    internal partial class DeviceNt1_1 : Hiperscan.SGS.Sgs1900.BaseDevice
    {
        public const byte CMD_REBOOT_BOOTLOADER = 3;
        private const double INTENSITY_SCALE_FACTOR = 1e-3;
        private const int TRANSFER_TIMEOUT_OVERHEAD = 500;  // overhead it takes from finsished sampling to finished usb transfer
        private const int TIMOUT_COMMAND_MS = 2000;
        private const int TIMOUT_LONGCOMMAND_MS = 5000;
        private const int TIMOUT_MSA_MS = 8000;
        private new const int LUT_LENGTH = 2800;
        //private const int USB_WRITE_READ_DELAY_MS = 50;

        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[]), 6, 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 WriteSerialNumber = new Command(AddressType.Master, 'S', 'W', typeof(string), 8, null);
            public static readonly Command ReadSerialNumber = new Command(AddressType.Master, 'S', 'R', null, 0, typeof(string));
            public static readonly Command WriteDeviceName = new Command(AddressType.Master, 'N', 'W', typeof(string), 20, null);
            public static readonly Command ReadDeviceName = new Command(AddressType.Master, 'N', 'R', null, 0, typeof(string));
            public static readonly Command WriteHardwareProperties = new Command(AddressType.Master, 'J', 'W', typeof(HwPropertyData), (UInt16)Marshal.SizeOf(typeof(HwPropertyData)), null);
            public static readonly Command ReadHardwareProperties = new Command(AddressType.Master, 'J', 'R', null, 0, typeof(HwPropertyData));
            public static readonly Command WriteUsbPid = new Command(AddressType.Master, 'P', 'I', typeof(UInt16), sizeof(UInt16), null);
            public static readonly Command WriteKey = new Command(AddressType.Master, 'K', 'W', typeof(KeyData), (UInt16)Marshal.SizeOf(typeof(KeyData)), null);
            public static readonly Command ReadKey = new Command(AddressType.Master, 'K', 'R', typeof(ChallengeData), (UInt16)Marshal.SizeOf(typeof(ChallengeData)), typeof(byte[]));
            public static readonly Command KeyIsProgrammed = new Command(AddressType.Master, 'K', 'P', typeof(byte), 1, typeof(byte));
            public static readonly Command ReadStatus = new Command(AddressType.Master, 'S', 'T', null, 0, typeof(StatusReadData));
            public static readonly Command ReadSystemLog = new Command(AddressType.Master, 'R', 'M', null, 0, typeof(string));
            public static readonly Command WriteHardwareVersion = new Command(AddressType.Master, 'S', 'V', typeof(byte[]), 2, null);
            public static readonly Command WriteTemperatureControlInfo = new Command(AddressType.Master, 'H', 'W', typeof(TcrtlInfoData), (UInt16)Marshal.SizeOf(typeof(TcrtlInfoData)), null);
            public static readonly Command ReadTemperatureControlInfo = new Command(AddressType.Master, 'H', 'R', null ,0, typeof(TcrtlInfoData));
            public static readonly Command ReadAdcValues = new Command(AddressType.Master,'A', 'V', typeof(byte[]), 3, typeof(ushort[]));
            public static readonly Command WriteOemSerialNumber = new Command(AddressType.Master, 'Q', 'W', typeof(string), 20 * sizeof(byte), null);
            public static readonly Command ReadOemSerialNumber = new Command(AddressType.Master, 'Q', 'R', null, 0, typeof(string));
            public static readonly Command OscillationMode = new Command(AddressType.Master, 'O', 'M', typeof(ushort), sizeof(ushort), null);
            public static readonly Command TestMode = new Command(AddressType.Master, 'T', 'M', typeof(byte[]), 3, null);

            // TODO: move this commands to a PPNT class
            public static readonly Command DetectPeripheral = new Command(AddressType.Master, 'F', 'D', null, 0, typeof(PeripheralInfoReadData));
            public static readonly Command PeripheralConnect = new Command(AddressType.Master, 'S', 'C', null, 0, null);
            public static readonly Command PeripheralSetWheelPosition = new Command(AddressType.Master, 'F', 'P', typeof(byte), sizeof(byte), null);
            public static readonly Command PeripheralSetLightSource = new Command(AddressType.Master, 'F', 'L', typeof(byte), sizeof(byte), null);
            public static readonly Command WriteFinderPcbHardwareVersion = new Command(AddressType.Master, 'F', 'C', typeof(byte[]), 4, null);
            public static readonly Command ReadFinderStatus = new Command(AddressType.Master, 'F', 'T', null, 0, typeof(StatusReadData));
            public static readonly Command FinderButton = new Command(AddressType.Master, 'F', 'S', null, 0, typeof(byte));
            public static readonly Command FinderStatusLED = new Command(AddressType.Master, 'F', 'B', typeof(byte), sizeof(byte),null);
        }

        public enum KeyIndex : byte
        {
            General = 0,
            Individual = 1
        }

        public enum TriggerMode : byte
        {
            Software = 0,
            Edge = 1,
            Gate = 2,
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        protected struct HwPropertyData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
            public byte[] Type;
            [MarshalAs(UnmanagedType.U2)] public UInt16 GridConstant;
            [MarshalAs(UnmanagedType.U2)] public UInt16 IncidenceAngle;
            [MarshalAs(UnmanagedType.U2)] public UInt16 DiffractionAngle;
            [MarshalAs(UnmanagedType.I2)] public Int16 MinTemperature;
            [MarshalAs(UnmanagedType.I2)] public Int16 MaxTemperature;
            [MarshalAs(UnmanagedType.U2)] public UInt16 SpectralResolution;
            [MarshalAs(UnmanagedType.U2)] public UInt16 WavelengthDrift;
            [MarshalAs(UnmanagedType.U4)] public UInt32 Sensitivity;
            [MarshalAs(UnmanagedType.U2)] public UInt16 SignalNoiseRatio;
            [MarshalAs(UnmanagedType.U2)] public UInt16 DynamicRange;

            public HwPropertyData(HardwareProperties hw) : this()
            {
                this.Type = new byte[2];

                if (hw.Type.Length == 1)
                    hw.Type += " ";

                if (hw.Type.Length == 2)
                {
                    this.Type[0] = (byte)hw.Type[0];
                    this.Type[1] = (byte)hw.Type[1];
                }
                else
                {
                    this.Type[0] = byte.MaxValue;
                    this.Type[1] = byte.MaxValue;
                }
                this.GridConstant = (UInt16)hw.GridConstant;
                this.IncidenceAngle = (UInt16)(hw.IncidenceAngle * 10.0);
                this.DiffractionAngle = (UInt16)(hw.DiffractionAngle * 10.0);
                this.MinTemperature = (Int16)(hw.MinTemperature * 100.0);
                this.MaxTemperature = (Int16)(hw.MaxTemperature * 100.0);
                this.SpectralResolution = (UInt16)(hw.SpectralResolution * 10.0);
                this.WavelengthDrift = (UInt16)(hw.WavelengthDrift * 10.0);
                this.Sensitivity = (UInt32)(hw.Sensitivity);
                this.SignalNoiseRatio = (UInt16)(hw.SignalNoiseRatio);
                this.DynamicRange = (UInt16)(hw.DynamicRange);
            }
        }

        [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 KeyData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.U1)] public KeyIndex Index;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = KEY_PAD_SIZE)]
            public byte[] Key;

            public KeyData(KeyIndex index, byte[] data) : this()
            {
                this.Index = index;
                this.Key = new byte[KEY_PAD_SIZE];
                Array.Copy(data, this.Key, data.Length);
            }
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        protected struct ChallengeData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.U1)] public KeyIndex Index;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = KEY_SIZE)]
            public byte[] Key;

            public ChallengeData(KeyIndex index, byte[] data) : this()
            {
                this.Index = index;
                this.Key = new byte[KEY_SIZE];
                Array.Copy(data, this.Key, data.Length);
            }
        }

        [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.U2)] public ushort Average;

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

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        internal struct StatusReadData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.I2)] public short Temperature;
            [MarshalAs(UnmanagedType.U2)] public ushort ErrorFlag;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        internal struct TcrtlInfoData : Hiperscan.SGS.SgsNt.Protocol.BaseProtocol.ITransferStruct
        {
            [MarshalAs(UnmanagedType.U1)] public byte Version;
            [MarshalAs(UnmanagedType.U1)] public byte ControlValue;
            [MarshalAs(UnmanagedType.I2)] public Int16 TargetTemperature;

            public TcrtlInfoData(byte version, byte control_value, double target_temperature) : this()
            {
                this.Version = version;
                this.ControlValue = control_value;
                this.TargetTemperature = (Int16)(target_temperature * 100.0);
            }
        }

        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.Channel1 | BaseProtocol.ChannelType.ReverseScan,
            BaseProtocol.ChannelType.Channel2,
            BaseProtocol.ChannelType.Channel2 | BaseProtocol.ChannelType.ReverseScan,
        };

        private readonly Protocol.ChannelType[] ALL_CHANNELS = new BaseProtocol.ChannelType[]
        {
                BaseProtocol.ChannelType.Channel1,
                BaseProtocol.ChannelType.Channel1 | BaseProtocol.ChannelType.ReverseScan,
                BaseProtocol.ChannelType.Channel2,
                BaseProtocol.ChannelType.Channel2 | BaseProtocol.ChannelType.ReverseScan,
                BaseProtocol.ChannelType.Channel3,
                BaseProtocol.ChannelType.Channel3 | BaseProtocol.ChannelType.ReverseScan,
                BaseProtocol.ChannelType.Channel4,
                BaseProtocol.ChannelType.Channel4 | BaseProtocol.ChannelType.ReverseScan,
        };

        private readonly string serial;
        private double scan_engine_msa_factor = double.NaN;
        private Protocol.ChannelType active_channel = BaseProtocol.ChannelType.Channel1;

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

                // workaround for #35154
                // usb stack not reading frame although it is sucessfully sent by device
                // FIXME: remove this!
                //Thread.Sleep(USB_WRITE_READ_DELAY_MS);

                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 = payload_data_len + BaseProtocol.CHECKSUM_LENGTH;

            int try_count = 0;

            while (true)
            {
                try
                {
                    byte[] buf = header.Concat(this.ReadBlocking(count, timeout, idle_task)).ToArray();
                    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));
                }
            }
        }

        protected byte[] ReadByteArrayParameter(Protocol.Command command, int timeout)
        {
            try
            {
                if (command.ResponseDataType != typeof(byte[]))
                    throw new SgsException(Catalog.GetString("Parameter data type 'byte[]' expected."), this);

                return this.WriteCommand(command, timeout).ByteArrayValue;
            }
            catch (Exception ex)
            {
                this.ExceptionHandler(Catalog.GetString("Cannot receive byte array parameter:") + " " + ex.Message, ex);
                return null;
            }
        }

        protected byte ReadByteParameter(Protocol.Command command, int timeout)
        {
            try
            {
                if (command.ResponseDataType != typeof(byte))
                    throw new SgsException(Catalog.GetString("Parameter data type 'byte' expected."), this);

                return this.WriteCommand(command, timeout).ByteValue;
            }
            catch (Exception ex)
            {
                this.ExceptionHandler(Catalog.GetString("Cannot receive byte array parameter:") + " " + ex.Message, ex);
                return 0;
            }
        }

        private Protocol.Response WriteStringParameter(Protocol.Command command, string data)
        {
            if (data.Length > command.PayloadLength)
                throw new SgsException(Catalog.GetString("Unsupported string length."), this);

            for (int ix = data.Length; ix < command.PayloadLength; ++ix)
            {
                data += " ";
            }

            command.StringValue = data;

            return this.WriteCommand(command, TIMOUT_COMMAND_MS);
        }

    }
}
