// Created with MonoDevelop
//
//    Copyright (C) 2017-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.Threading;

using Hiperscan.SGS.Common;
using Hiperscan.Unix;

using LibUsbDotNet;
using LibUsbDotNet.Main;

using Version = Hiperscan.SGS.Common.Version;


namespace Hiperscan.SGS.SerialInterface.USB
{
    public class FT245BL : Hiperscan.SGS.SerialInterface.USB.LibUsbDevice, Hiperscan.SGS.SerialInterface.ISerialInterface
    {

        private const int READBUFFER_SIZE = 16384;
        /* From libftdi 1.4 sources:
         * We can't set readbuffer_chunksize larger than MAX_BULK_BUFFER_LENGTH,
         * which is defined in libusb-1.0. Otherwise, each USB read request will
         * be divided into multiple URBs. This will cause issues on Linux kernel
         * older than 2.6.32.  
         *
         * A readbuffer size greater than 16384 seems to cause still trouble on
         * ARM hardware. See tickets #15643, #33562        
         */

        private const int WRITE_CHUNKSIZE = 4096;
        private const int FTDI_DEVICE_OUT_REQTYPE = 64;

        private const byte SIO_RESET_REQUEST = 0;
        private const int SIO_RESET_SIO = 0;
        private const int SIO_SET_BAUDRATE_REQUEST = 3;
        private const int BAUDRATE = 16696;
        private const int MAX_PACKET_SIZE = 64;
        private const int VERSION_READ_TIMEOUT_MS = 1000;

        private int readbuffer_remaining;
        private int readbuffer_offset;
        private readonly byte[] readbuffer = new byte[READBUFFER_SIZE];


        public FT245BL(IEnumerable<Tuple<uint,uint>> vendor_product_ids)
        {
            this.vendor_product_ids = vendor_product_ids.ToArray();
        }

        public FT245BL(IEnumerable<SerialInterfaceConfig> usb_configs) : this(usb_configs.Select(c => Tuple.Create(c.VendorId, c.ProductId)))
        {
        }

        public FT245BL(uint vendor_id, uint product_id) : this(new Tuple<uint,uint>[] { Tuple.Create(vendor_id, product_id) })
        {
        }

        protected override void OpenUsbDevice(string serial)
        {
            this.usb_device = UsbDevice.OpenUsbDevice((dev) =>
                                                      this.vendor_product_ids.Any(v => v.Item1 == dev.Vid && v.Item2 == (uint)dev.Pid) &&
                                                      LibUsbDevice.GetSerialNumber(dev) == serial);

            if (this.usb_device == null)
                throw new Exception(Catalog.GetString("Cannot open device."));

            if (this.usb_device is IUsbDevice iusb_device)
            {
                if (iusb_device.ClaimInterface(0) == false)
                    throw new Exception(Catalog.GetString("Cannot claim interface."));
            }

            UsbSetupPacket usb_reset = new UsbSetupPacket(FTDI_DEVICE_OUT_REQTYPE,
                                                          SIO_RESET_REQUEST,
                                                          SIO_RESET_SIO,
                                                          0x0000,
                                                          0x0000);

            bool success = this.usb_device.ControlTransfer(ref usb_reset,
                                                           new byte[0],
                                                           0,
                                                           out int transferred);
            if (success == false || transferred != 0)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(usb_reset));
        }

        protected override void InitUsbDevice()
        {
            UsbSetupPacket set_baudrate = new UsbSetupPacket(FTDI_DEVICE_OUT_REQTYPE,
                                                             SIO_SET_BAUDRATE_REQUEST,
                                                             BAUDRATE,
                                                             0x0000,
                                                             0x0000);

            bool success = this.usb_device.ControlTransfer(ref set_baudrate,
                                                           new byte[0],
                                                           0,
                                                           out int transferred);
            if (success == false || transferred != 0)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(set_baudrate));

            this.SetTimeouts();
            this.SetLatency();

            this.endpoint_reader = this.usb_device.OpenEndpointReader(ReadEndpointID.Ep01);
            this.endpoint_writer = this.usb_device.OpenEndpointWriter(WriteEndpointID.Ep02);
        }

        public override void ResetDevice()
        {
            if (string.IsNullOrEmpty(this.serial))
                throw new Exception(Catalog.GetString("Cannot reset a USB device which has not been opened."));

            this.usb_device.Close();
            Thread.Sleep(1000);
            this.OpenUsbDevice(this.serial);
            this.InitUsbDevice();
        }

        public override uint Read(out byte[] rx_buf, uint size)
        {
            int offset = 0, ix, chunk_count, chunk_remains;
            int packet_size = MAX_PACKET_SIZE;
            int transferred = 1;
            rx_buf = new byte[size];

            // everything we want is still in the readbuffer?
            if (size <= this.readbuffer_remaining)
            {
                Buffer.BlockCopy(this.readbuffer, this.readbuffer_offset, rx_buf, 0, (int)size);

                this.readbuffer_remaining -= (int)size;
                this.readbuffer_offset    += (int)size;

                return size;
            }

            // something still in the readbuffer, but not enough to satisfy 'size'?
            if (this.readbuffer_remaining != 0)
            {
                Buffer.BlockCopy(this.readbuffer, this.readbuffer_offset, rx_buf, 0, this.readbuffer_remaining);

                offset += this.readbuffer_remaining;
            }

            // do the actual USB read
            while (offset < size && transferred > 0)
            {
                this.readbuffer_remaining = 0;
                this.readbuffer_offset    = 0;

                ErrorCode error_code = this.endpoint_reader.Transfer(this.readbuffer,
                                                                     0,
                                                                     this.readbuffer.Length,
                                                                     this.config.ReadTimeout,
                                                                     out transferred);

                if (error_code != ErrorCode.None && error_code != ErrorCode.IoTimedOut)
                    throw new UsbException(error_code, Catalog.GetString("Cannot execute bulk transfer (read):") + " " + UsbDevice.LastErrorString);

                if (transferred > 2)
                {
                    // skip FTDI status bytes.
                    chunk_count   = transferred / packet_size;
                    chunk_remains = transferred % packet_size;

                    this.readbuffer_offset += 2;
                    transferred -= 2;

                    if (transferred > packet_size - 2)
                    {
                        for (ix = 1; ix < chunk_count; ix++)
                        {
                            Buffer.BlockCopy(this.readbuffer, this.readbuffer_offset + packet_size * ix, 
                                             this.readbuffer, this.readbuffer_offset + (packet_size - 2) * ix, 
                                             packet_size - 2);
                        }

                        if (chunk_remains > 2)
                        {
                            Buffer.BlockCopy(this.readbuffer, this.readbuffer_offset + packet_size * ix,
                                             this.readbuffer, this.readbuffer_offset + (packet_size - 2) * ix,
                                             chunk_remains - 2);
                            transferred -= 2 * chunk_count;
                        }
                        else
                        {
                            transferred -= 2 * (chunk_count - 1) + chunk_remains;
                        }
                    }
                }
                else if (transferred <= 2)
                {
                    // no more data to read?
                    rx_buf = rx_buf.Take(offset).ToArray();
                    return (uint)offset;
                }

                if (transferred > 0)
                {
                    // data still fits in buf?
                    if (offset + transferred <= size)
                    {
                        Buffer.BlockCopy(this.readbuffer, this.readbuffer_offset, rx_buf, offset, transferred);
                        offset += transferred;

                        // did we read exactly the right amount of bytes?
                        if (offset == size)
                        {
                            rx_buf = rx_buf.Take(offset).ToArray();
                            return (uint)offset;
                        }
                    }
                    else
                    {
                        // only copy part of the data or size <= readbuffer_chunksize
                        int part_size = (int)size - offset;
                        Buffer.BlockCopy(this.readbuffer, this.readbuffer_offset, rx_buf, offset, part_size);

                        this.readbuffer_offset += part_size;
                        this.readbuffer_remaining = transferred - part_size;
                        offset += part_size;

                        rx_buf = rx_buf.Take(offset).ToArray();
                        return (uint)offset;
                    }
                }
            }

            throw new Exception("This should be never reached.");
        }

        public uint Read(out byte[] data)
        {
            List<byte> rx_buf = new List<byte>();

            while (true)
            {
                byte[] buf = null;
                try
                {
                    this.Read(out buf, READBUFFER_SIZE);
                }
                catch (UsbException ex)
                {
                    if (ex.Status != ErrorCode.IoTimedOut.ToString())
                        throw;
                }

                if (buf?.Length > 0)
                    rx_buf.AddRange(buf);
                else
                    break;
            }

            data = rx_buf.ToArray();

            return (uint)rx_buf.Count;
        }

        public UInt32 Write(byte[] data)
        {
            int offset = 0;
            int size = data.Length;

            while (offset < size)
            {
                int write_size = WRITE_CHUNKSIZE;

                if (offset + write_size > size)
                    write_size = size - offset;

                ErrorCode error_code = this.endpoint_writer.Transfer(data,
                                                                     offset,
                                                                     write_size,
                                                                     this.config.WriteTimeout,
                                                                     out int transferred);

                if (error_code != ErrorCode.None || transferred != write_size)
                    throw new UsbException(error_code, Catalog.GetString("Cannot execute bulk transfer (write):") + " " + UsbDevice.LastErrorString);

                offset += transferred;
            }

            return (uint)offset;
        }

        internal static VersionInfo  GetVersionInfo(DeviceInfo info, bool disable_timeout)
        {
            FT245BL usb = new FT245BL(info.VendorId, info.ProductId);

            usb.OpenBySerialNumber(info.SerialNumber);

            int timeout = VERSION_READ_TIMEOUT_MS;
            if (disable_timeout)
                timeout = 0;

            // protocol version
            byte[] pv_buf = new byte[] { 0, 0 };
            for (int count = 0; count < 20; ++count)
            {
                try
                {
                    usb.ReadBlocking(5000, 10, null);
                }
                catch (TimeoutException) { }

                try
                {
                    usb.Write(new byte[] { 0 });
                    pv_buf = usb.ReadBlocking(2, timeout, null);
                    break;
                }
                catch (Exception ex) when (ex is TimeoutException || (ex as UsbException)?.Status == ErrorCode.IoTimedOut.ToString())
                {
                    if (timeout == 0)
                        break;
                }
            }

            Version protocol_version = new Version(pv_buf[0], pv_buf[1]);

            byte[] fw_buf = new byte[] { 0, 0 };
            byte[] se_buf = new byte[] { 0, 0 };
            byte[] hw_buf = new byte[] { 0, 0 };

            if (protocol_version.Id >= (ushort)Common.Version.VersionId.v0_15)
            {
                // firmware version
                try
                {
                    usb.Write(new byte[] { 1 });
                    fw_buf = usb.ReadBlocking(2, timeout, null);
                    // workaround for protocol bug (some devices implement command "1" as part of outdated Spectragon protocol)
                    usb.ReadBlocking(2, 10, null);
                }
                catch (TimeoutException) { }

                Version cur_version = new Version(fw_buf[0], fw_buf[1]);
                Version min_version = new Version(38, 12);

                if ((ushort)cur_version.Id >= (ushort)min_version.Id)
                {
                    // scan engine version
                    try
                    {
                        usb.Write(new byte[] { 2 });
                        se_buf = usb.ReadBlocking(2, timeout, null);
                    }
                    catch (TimeoutException) { }
                }

                min_version = new Version(38, 17);
                if ((ushort)cur_version.Id >= (ushort)min_version.Id)
                {
                    // hardware version
                    try
                    {
                        usb.Write(new byte[] { 4 });
                        hw_buf = usb.ReadBlocking(2, timeout, null);
                    }
                    catch (TimeoutException) { }
                }
            }

            usb.Close();

            Console.WriteLine("Protocol version: {0}.{1}",    pv_buf[0], pv_buf[1]);
            Console.WriteLine("Firmware version: {0}.{1}",    fw_buf[0], fw_buf[1]);
            Console.WriteLine("Scan Engine version: {0}.{1}", se_buf[0], se_buf[1]);
            Console.WriteLine("Hardware version: {0}.{1}",    hw_buf[0], hw_buf[1]);

            Version firmware_version    = new Version(fw_buf[0], fw_buf[1]);
            Version scan_engine_version = new Version(se_buf[0], se_buf[1]);
            Version hardware_version    = new Version(hw_buf[0], hw_buf[1]);

            return new VersionInfo(protocol_version, firmware_version, scan_engine_version, hardware_version);
        }

        protected override void ResetUsbDevice()
        {
        }
    }
}
