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

using LibUsbDotNet;
using LibUsbDotNet.Main;

using Hiperscan.Unix;


namespace Hiperscan.SGS
{

	public class CP2130 : Hiperscan.SGS.IUSB
	{

		[Serializable()]
		public class USBException: System.Exception, System.Runtime.Serialization.ISerializable
		{
			private string status;

			public USBException(ErrorCode error_code, string message) : base(message)
			{
				this.status = Enum.GetName(typeof(ErrorCode), error_code);
			}

			public USBException(Exception ex) : base(ex.Message, ex)
			{
			}

			public USBException(SerializationInfo info, StreamingContext context) : base(info, context)
			{
				this.status = (string)info.GetValue("Status", typeof(string));
			}

			public new void GetObjectData(SerializationInfo info, StreamingContext ctxt)
			{
				base.GetObjectData(info, ctxt);
				info.AddValue("Status", this.status);
			}

			public string Status
			{
				get { return status; }
			}
		}

		public class Config
		{
			public byte  LatencyTime    = 100;  // refreshrate USB (1-255ms)
			public int   ReadTimeout    = 2500; // 2.5s USB Timeout ( rcv )
			public int   WriteTimeout   = 2500; // 2.5s USB Timeout ( send )
			public uint  ReadBufferSize = 4096;
		}		

		private uint vendor_id;
		private uint[] product_ids;
		private UsbDevice usb_device;
		private UsbEndpointReader endpoint_reader;
		private UsbEndpointWriter endpoint_writer;
		private Config config = new Config();


		public CP2130(uint vendor_id, uint[] product_ids)
		{
			this.vendor_id  = vendor_id;
			this.product_ids = product_ids;
		}

		public CP2130(uint vendor_id, uint product_id) : this(vendor_id, new uint[] { product_id })
		{
		}

		public uint GetNumberOfDevices()
		{
			return (uint)UsbDevice.AllDevices.FindAll((usb_reg) => usb_reg.Vid == this.vendor_id && this.product_ids.Contains((uint)usb_reg.Pid)).Count;
		}

		public DeviceInfo[] GetDeviceList()
		{
			List<DeviceInfo> infos = new List<DeviceInfo>();

			var usb_regs = UsbDevice.AllDevices.FindAll((dev) => dev.Vid == this.vendor_id && this.product_ids.Contains((uint)dev.Pid));
			foreach (UsbRegistry usb_reg in usb_regs)
			{
				string manufacturer = usb_reg.DeviceProperties["Mfg"].ToString();
				string description  = usb_reg.DeviceProperties["DeviceDesc"].ToString();
				string serial = CP2130.GetSerialNumber(usb_reg);

				infos.Add(new DeviceInfo(manufacturer, serial, description, (uint)usb_reg.Vid, (uint)usb_reg.Pid, DeviceType.SGSNT));
			}

			return infos.ToArray();
		}

		public void OpenBySerialNumber(string serial)
		{
			try
			{
				this.usb_device = UsbDevice.OpenUsbDevice((usb_reg) => 
				                                          usb_reg.Vid == this.vendor_id &&
				                                          this.product_ids.Contains((uint)usb_reg.Pid) &&
				                                          CP2130.GetSerialNumber(usb_reg) == serial);

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

				IUsbDevice iusb_device = this.usb_device as IUsbDevice;
				if (iusb_device != null)
				{
					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."));
				}

				int transferred;
				bool success;

				// see AN792, page 28
				UsbSetupPacket set_gpio_chip_select = new UsbSetupPacket(0x40, 0x25, 0x0000, 0x0000, 0x0002);
				byte[] cs1_enable_disable_others = new byte[2] { 0x01, 0x02 };
				success = usb_device.ControlTransfer(ref set_gpio_chip_select, cs1_enable_disable_others, cs1_enable_disable_others.Length, out transferred);
				if (success == false || transferred != cs1_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(0x40, 0x31, 0x0000, 0x0000, 0x0002);
				byte[] cs1_leading_edge_idle_low_push_pull_3mhz = new byte[2] { 0x01, 0b0000_1010 };
				success = usb_device.ControlTransfer(ref set_spi_word, cs1_leading_edge_idle_low_push_pull_3mhz, cs1_leading_edge_idle_low_push_pull_3mhz.Length, out transferred);
				if (success == false || transferred != cs1_leading_edge_idle_low_push_pull_3mhz.Length)
					throw new USBException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(set_spi_word));

				this.endpoint_reader = this.usb_device.OpenEndpointReader(ReadEndpointID.Ep02);
				this.endpoint_writer = this.usb_device.OpenEndpointWriter(WriteEndpointID.Ep01);
			}
			catch (Exception ex)
			{
				string msg = string.Format(Catalog.GetString("Cannot access USB device with serial " +
				                                             "number {0}: Device may already be in use."),
				                           serial);
				throw new USBException(ErrorCode.ResourceBusy, msg + " (" + ex.Message + ")");
			}
		}

		public void Close()
		{
			if (this.usb_device == null)
				throw new USBException(ErrorCode.None, Catalog.GetString("Cannot close USB device which has not been initialized."));
			try
			{
				this.usb_device.Close();
			}
			catch (Exception ex)
			{
				throw new USBException(ErrorCode.None, Catalog.GetString("Cannot close USB device:") + " " + ex.Message);
			}
		}

		public void ResetDevice()
		{
			// see AN792, page 24
			UsbSetupPacket reset_device = new UsbSetupPacket(0x40, 0x10, 0x0000, 0x0000, 0x0000);
			byte[] empty = new byte[0];
			int transferred;
			bool success = this.usb_device.ControlTransfer(ref reset_device, empty, empty.Length, out transferred);
			if (success == false || transferred != empty.Length)
				throw new USBException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(reset_device));
		}

		public void SetTimeouts(int read_timeout, int write_timeout)
		{
			this.config.ReadTimeout  = read_timeout;
			this.config.WriteTimeout = write_timeout;
		}

		public void SetTimeouts()
		{
			this.SetTimeouts(this.config.ReadTimeout, this.config.WriteTimeout);
		}

		public void SetLatency()
		{
		}

		public void Purge()
		{
		}

		public uint Read(out byte[] data, uint len)
		{
			if (this.DataAvailable == false)
			{
				data = new byte[0];
				return 0;
			}
			
			// see AN792, page 8
			byte[] read_cmd_buf = new byte[8]
			{
				0x00, 0x00,                  // reserved
				0x00,                        // read command
				0x00,                        // reserved
				(byte)(len >> 0  & 0xFF), 
				(byte)(len >> 8  & 0xFF), 
				(byte)(len >> 16 & 0xFF), 
				(byte)(len >> 24 & 0xFF), 
			};
			ErrorCode error_code;
			int transferred;
			data = new byte[len];
			error_code = this.endpoint_writer.Transfer(read_cmd_buf, 0, read_cmd_buf.Length, this.config.WriteTimeout, out transferred);
			if (error_code != ErrorCode.None || transferred != read_cmd_buf.Length)
				throw new USBException(error_code, Catalog.GetString("Cannot execute bulk transfer (write):") + " " + UsbDevice.LastErrorString);
			
			error_code = this.endpoint_reader.Transfer(data, 0, data.Length, this.config.ReadTimeout, out transferred);
			if (error_code != ErrorCode.None || transferred != data.Length)
				throw new USBException(error_code, Catalog.GetString("Cannot execute bulk transfer (read):") + " " + UsbDevice.LastErrorString);

			return (uint)transferred;
		}

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

			for (;;)
			{
				byte[] buf = null;
				try
				{
					this.Read(out buf, this.config.ReadBufferSize);
				}
				catch (USBException ex)
				{
					if (ex.Status != ErrorCode.IoTimedOut.ToString())
						throw new USBException(ex);
				}

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

			data = rx_buf.ToArray();

			return (uint)rx_buf.Count;
		}

		public byte[] ReadBlocking(uint len, int timeout, IdleTaskHandler idle_task)
		{
			List<byte[]> buffers = new List<byte[]>();
			DateTime then = DateTime.Now;

			uint count = 0;

			while (count < len)
			{
				byte[] rx_buf = null;
				uint result;

				result = this.Read(out rx_buf, len-count);

				if (result  > 0)
				{
					Console.WriteLine("ReadBlocking: {0}/{1}", result, len);
					buffers.Add(rx_buf);
					count += result;
					continue;
				}

				TimeSpan span = DateTime.Now - then;

				if (timeout > 0 && span.TotalMilliseconds > timeout)
				{
					Console.WriteLine("Timeout while ReadBlocking: {0}/{1}", result, len);
					foreach (byte[] buf in buffers)
					{
						Console.Write("\talready read:");
						foreach (byte b in buf)
							Console.Write(" 0x{0:X2}", b);
						Console.WriteLine();
					}
					throw new TimeoutException(Catalog.GetString("Timeout while blocking read."));
				}

				if (idle_task != null)
					idle_task();

				System.Threading.Thread.Sleep(50);
			}

			byte[] data = new byte[count];

			int offset = 0;
			foreach (byte[] buf in buffers)
			{
				Array.Copy(buf, 0, data, offset, buf.Length);
				offset += buf.Length;
			}	
			return data;
		}		

		public uint Write(byte[] data)
		{
			int len = data.Length;
			int transferred;
			ErrorCode error_code;

			// see AN792, page 9
			byte[] write_cmd_buf = new byte[8]
			{ 
				0x00, 0x00,                // reserved
				0x01,                      // 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 = endpoint_writer.Transfer(tx_buf, 0, tx_buf.Length, this.config.WriteTimeout, out 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 (uint)(transferred - write_cmd_buf.Length);
		}

		public DriverType DriverType
		{
			get
			{
				return DriverType.LibUSB;
			}
		}

		private bool DataAvailable
		{
			get
			{
				// see AN792, page 18
				UsbSetupPacket get_gpio_values = new UsbSetupPacket(0xC0, 0x20, 0x0000, 0x0000, 0x0002);
				int transferred;
				byte[] gpio_levels = new byte[2];
				bool success = this.usb_device.ControlTransfer(ref get_gpio_values, gpio_levels, gpio_levels.Length, out transferred);
				if (success == false || transferred != gpio_levels.Length)
					throw new USBException(ErrorCode.None, Catalog.GetString("Cannot execute control transfer:") + " " + nameof(get_gpio_values));

				return (gpio_levels[0] & 0x04) == 0;
			}
		}

		private static string GetSerialNumber(UsbRegistry usb_reg)
		{
			if (usb_reg.DeviceProperties.ContainsKey("SerialNumber"))
			{
				return usb_reg.DeviceProperties["SerialNumber"].ToString();
			}
			else if (usb_reg.DeviceProperties.ContainsKey("DeviceID"))
			{
				string device_id = usb_reg.DeviceProperties["DeviceID"].ToString();
				return device_id.Substring(device_id.LastIndexOf('\\')).Trim('\\');
			}
			else
			{
				throw new USBException(ErrorCode.NotSupported, Catalog.GetString("Cannot determine serial number of device."));
			}
		}
	}
}
