// 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 CP2130 : Hiperscan.SGS.SerialInterface.USB.LibUsbDevice, Hiperscan.SGS.SerialInterface.ISerialInterface
    {
        private const byte REQUEST_TYPE_DEVICE_TO_HOST_VENDOR_REQUEST = 0xC0;
        private const byte REQUEST_TYPE_HOST_TO_DEVICE_VENDOR_REQUEST = 0x40;

        private const UInt16 GPIO_MASTER_IS_IDLE = 0x04;
        private const Int32  GPIO_VALUE_LEN = 2;

        private const byte COMMAND_ID_GET_GPIO = 0x20;
        private const byte COMMAND_ID_SET_GPIO_CHIP_SELECT = 0x25;
        private const byte COMMAND_ID_SET_SPI_WORD = 0x31;
        private const byte COMMAND_ID_SET_SPI_DELAY = 0x33;
        private const byte COMMAND_ID_RESET_DEVICE = 0x10;
        private const byte COMMAND_ID_READ = 0x00;
        private const byte COMMAND_ID_WRITE = 0x01;


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

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

        public CP2130(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.SetConfiguration(1) == false)
                    throw new Exception(Catalog.GetString("Cannot set configuration."));

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

        protected override void InitUsbDevice()
        {
            bool success;

            // see AN792, page 28
            UsbSetupPacket set_gpio_chip_select = new UsbSetupPacket(REQUEST_TYPE_HOST_TO_DEVICE_VENDOR_REQUEST,
                                                                     COMMAND_ID_SET_GPIO_CHIP_SELECT,
                                                                     0x0000, 
                                                                     0x0000,
                                                                     0x0002);

            byte[] cs0_enable_disable_others = new byte[2] { 0x00, 0x02 };

            success = this.usb_device.ControlTransfer(ref set_gpio_chip_select, 
                                                      cs0_enable_disable_others,
                                                      cs0_enable_disable_others.Length,
                                                      out int transferred);

            if (success == false || transferred != cs0_enable_disable_others.Length)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(set_gpio_chip_select));

            // see AN792, page 32
            UsbSetupPacket set_spi_word = new UsbSetupPacket(REQUEST_TYPE_HOST_TO_DEVICE_VENDOR_REQUEST,
                                                             COMMAND_ID_SET_SPI_WORD,
                                                             0x0000,
                                                             0x0000,
                                                             0x0002);

            byte[] cs0_leading_edge_idle_low_push_pull_3mhz = new byte[2] { 0x00, 0b0000_1010 };

            success = this.usb_device.ControlTransfer(ref set_spi_word,
                                                      cs0_leading_edge_idle_low_push_pull_3mhz,
                                                      cs0_leading_edge_idle_low_push_pull_3mhz.Length,
                                                      out transferred);

            if (success == false || transferred != cs0_leading_edge_idle_low_push_pull_3mhz.Length)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(set_spi_word));

            // see AN792, page 33
            UsbSetupPacket set_spi_delay = new UsbSetupPacket(REQUEST_TYPE_HOST_TO_DEVICE_VENDOR_REQUEST,
                                                              COMMAND_ID_SET_SPI_DELAY,
                                                              0x0000,
                                                              0x0000,
                                                              0x0008);

            UInt16 post_assert_delay = 1500/10; // 1500us
            UInt16 pre_assert_delay  = 10/10;   // 10us
            byte[] post_assert_delay_buf = BitConverter.GetBytes(post_assert_delay);
            byte[] pre_assert_delay_buf  = BitConverter.GetBytes(pre_assert_delay);

            byte[] post_assert_500u_pre_assert_100u = new byte[8] { 0x00, 0b0000_0110, 0x00, 0x00, 
                                                                    post_assert_delay_buf[1], post_assert_delay_buf[0], 
                                                                    pre_assert_delay_buf[1],  pre_assert_delay_buf[0] };

            success = this.usb_device.ControlTransfer(ref set_spi_delay,
                                                      post_assert_500u_pre_assert_100u,
                                                      post_assert_500u_pre_assert_100u.Length,
                                                      out transferred);

            if (success == false || transferred != post_assert_500u_pre_assert_100u.Length)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(set_spi_delay));
        
            this.SetTimeouts();
            this.SetLatency();

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

            // perform small write operation to 
            this.Write(new byte[1]);
        }

        protected override void ResetUsbDevice()
        {
            // see AN792, page 24
            UsbSetupPacket reset_device = new UsbSetupPacket(REQUEST_TYPE_HOST_TO_DEVICE_VENDOR_REQUEST,
                                                             COMMAND_ID_RESET_DEVICE,
                                                             0x0000,
                                                             0x0000,
                                                             0x0000);

            byte[] empty = new byte[0];
            bool success = this.usb_device.ControlTransfer(ref reset_device, empty, empty.Length, out int transferred);

            if (success == false || transferred != empty.Length)
                throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(reset_device));
        }

        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.ResetUsbDevice();
            this.usb_device.Close();
            Thread.Sleep(1000);
            this.OpenUsbDevice(this.serial);
            this.InitUsbDevice();
        }

        public override uint Read(out byte[] rx_buf, uint size)
        {
            if (this.IsBusy)
            {
                rx_buf = new byte[0];
                return 0;
            }
            
            // see AN792, page 8
            byte[] read_cmd_buf = new byte[8]
            {
                0x00, 0x00,                  // reserved
                COMMAND_ID_READ,             // read command
                0x00,                        // reserved
                (byte)(size >> 0  & 0xFF), 
                (byte)(size >> 8  & 0xFF), 
                (byte)(size >> 16 & 0xFF), 
                (byte)(size >> 24 & 0xFF), 
            };

            ErrorCode error_code;
            rx_buf = new byte[size];

            error_code = this.endpoint_writer.Transfer(read_cmd_buf, 
                                                       0,
                                                       read_cmd_buf.Length,
                                                       this.config.WriteTimeout,
                                                       out int write_transfered);

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

            error_code = this.endpoint_reader.Transfer(rx_buf,
                                                       0,
                                                       rx_buf.Length,
                                                       this.config.ReadTimeout,
                                                       out int read_transferred);

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

            return (uint)read_transferred;
        }

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

            while (true)
            {
                byte[] buf = null;
                try
                {
                    this.Read(out buf, this.config.ReadBufferSize);
                }
                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)
        {
            DateTime then = DateTime.Now;
            TimeSpan span = new TimeSpan(0, 0, 0, 0, 1000);

            while (this.IsBusy)
            {
                if (DateTime.Now - then > span)
                    throw new TimeoutException(Catalog.GetString("Timeout while USB bulk transfer (write)."));

                Thread.Sleep(5);
            }

            return this._Write(data);
        }

        private uint _Write(byte[] data)
        {
            Int32 len = data.Length;
            ErrorCode error_code;

            // see AN792, page 9
            byte[] write_cmd_buf = new byte[8]
            {
                0x00, 0x00,                // reserved
                COMMAND_ID_WRITE,          // write command
                0x00,                      // reserved
                (byte)(len >> 0  & 0xFF),
                (byte)(len >> 8  & 0xFF),
                (byte)(len >> 16 & 0xFF),
                (byte)(len >> 24 & 0xFF),
            };

            byte[] tx_buf = write_cmd_buf.Concat(data).ToArray();

            error_code = this.endpoint_writer.Transfer(tx_buf,
                                                       0,
                                                       tx_buf.Length,
                                                       this.config.WriteTimeout,
                                                       out int transferred);

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

            return (UInt32)(transferred - write_cmd_buf.Length);
        }

        private bool IsBusy
        {
            get
            {
                // see AN792, page 18
                UsbSetupPacket get_gpio_values = new UsbSetupPacket(REQUEST_TYPE_DEVICE_TO_HOST_VENDOR_REQUEST,
                                                                    COMMAND_ID_GET_GPIO,
                                                                    0,
                                                                    0,
                                                                    GPIO_VALUE_LEN);

                byte[] buf = new byte[GPIO_VALUE_LEN];
                bool success = this.usb_device.ControlTransfer(ref get_gpio_values,
                                                               buf,
                                                               buf.Length,
                                                               out int transferred);

                if (success == false || transferred != buf.Length)
                    throw new UsbException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(get_gpio_values));
                UInt16 gpio_levels = BitConverter.ToUInt16(buf, 0);

                //if ((gpio_levels & GPIO_MASTER_IS_IDLE) != GPIO_MASTER_IS_IDLE)
                //    Console.WriteLine("BUSY!");
                //else
                    //Console.WriteLine("Not BUSY...");

                return (gpio_levels & GPIO_MASTER_IS_IDLE) != GPIO_MASTER_IS_IDLE;
            }
        }

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

            usb.OpenBySerialNumber(info.SerialNumber);

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

            // protocol version
            byte[] pv_buf = new byte[] { 0, 0 };
            for (int count = 0; count < 20; ++count)
            {
                try
                {
                    usb.Write(new byte[] { 0 });
                    pv_buf = usb.ReadBlocking(2, timeout, null);
                    break;
                }
                catch (TimeoutException)
                {
                    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 };

            // firmware version
            usb.Write(new byte[] { 1 });
            fw_buf = usb.ReadBlocking(2, timeout, null);

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

            // scan engine version
            usb.Write(new byte[] { 2 });
            se_buf = usb.ReadBlocking(2, timeout, null);

            // hardware version
            usb.Write(new byte[] { 4 });
            hw_buf = usb.ReadBlocking(2, timeout, null);

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