// Created with MonoDevelop
//
//    Copyright (C) 2009 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.Text.RegularExpressions;
using System.Runtime.Serialization;
using System.Threading;


using FTD2XX_NET4libusb;
using Hiperscan.Unix;


namespace Hiperscan.SGS
{
	internal class FT245BL : IUSB
	{
		
		[Serializable()]
		public class USBException: System.Exception, System.Runtime.Serialization.ISerializable
		{
			private string status;
			
			public USBException(FTD2XX4libusb.FT_STATUS status, string message) : base(message)
			{
				this.status = Enum.GetName(typeof(FTD2XX4libusb.FT_STATUS), status);
				//Console.WriteLine("USB status: " + status);
			}

			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 struct Config
		{
			public const byte LatencyTime    = 100;  // refreshrate USB (1-255ms)
			public const int  ReadTimeout    = 2500; // 2.5s USB Timeout ( rcv )
			public const int  WriteTimeout   = 2500; // 2.5s USB Timeout ( send )
			public const int  ReadBufferSize = 4096;
		}		
		
		private FTD2XX4libusb ftdi4libusb;

		public FT245BL(uint vendor_id, uint[] product_ids)
		{
			this.ftdi4libusb = new FTD2XX4libusb();
			this.ftdi4libusb.VendorId  = vendor_id;
			this.ftdi4libusb.ProductIds = product_ids;
		}

		public FT245BL(uint vendor_id, uint product_id) : this(vendor_id, new uint[] { product_id })
		{
		}
		
		public uint GetNumberOfDevices()
		{
			uint count = 0;
			
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.GetNumberOfDevices(ref count);
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot get number of devices."));
			
			return count;
		}
		
		public DeviceInfo[] GetDeviceList()
		{
			DeviceInfo[] infos;
			
			uint count = this.GetNumberOfDevices();

			FTD2XX4libusb.FT_DEVICE_INFO_NODE[] nodes 
				= new FTD2XX4libusb.FT_DEVICE_INFO_NODE[count];
			
			bool success = false;
            int try_count = 0;
			while (!success)
			{
				try
				{
					FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.GetDeviceList(nodes);

                    if (status == FTD2XX4libusb.FT_STATUS.FT_IO_ERROR && try_count++ < 5)
                    {
                        Thread.Sleep(50);
                        continue;
                    }

					if (status != FTD2XX4libusb.FT_STATUS.FT_OK && status != FTD2XX4libusb.FT_STATUS.FT_DEVICE_NOT_FOUND)
                        throw new USBException(status, Catalog.GetString("Cannot get device list.") + " status=" + status.ToString());
					
					success = true;
					
					// a device could be removed just after GetNumberOfDevices() was called 
					foreach (FTD2XX4libusb.FT_DEVICE_INFO_NODE node in nodes)
					{
						if (node == null)
							success = false;
					}
				}
				catch (System.IndexOutOfRangeException)
				{
					count = this.GetNumberOfDevices();
					nodes = new FTD2XX4libusb.FT_DEVICE_INFO_NODE[count];
				}
			}
			
			infos = new DeviceInfo[nodes.Length];
			for (int ix=0; ix < nodes.Length; ++ix)
			{
				infos[ix] = new DeviceInfo(nodes[ix]);
			}

			return infos;
		}
		
		public void OpenBySerialNumber(string serial)
		{
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.OpenBySerialNumber(serial);
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
			{
				string msg = string.Format(Catalog.GetString("Cannot open USB device with serial " +
				                                             "number {0}: Device may already be in use."),
				                           serial);
				throw new USBException(status, msg);
			}
        }
		
		public void Close()
		{
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.Close();
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot close USB device."));
		}

		public void ResetDevice()
		{
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.ResetDevice();
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot reset USB device."));
		}

		public void SetTimeouts(int read_timeout, int write_timeout)
		{
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.SetTimeouts((uint)read_timeout, (uint)write_timeout);
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot set USB timeouts."));
		}
		
		public void SetTimeouts()
		{
			this.SetTimeouts(Config.ReadTimeout, Config.WriteTimeout);
		}

		public void SetLatency()
		{
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.SetLatency(Config.LatencyTime);
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot set USB latency."));
		}
		
		public void Purge()
		{
			FTD2XX4libusb.FT_STATUS status =
				this.ftdi4libusb.Purge(FTD2XX4libusb.FT_PURGE.FT_PURGE_RX | FTD2XX4libusb.FT_PURGE.FT_PURGE_TX);
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot purge USB rx buffer."));
		}

		public uint Read(out byte[] data, uint len)
		{
			byte[] rx_buf = new byte[len];
			uint count = 0;
			
			FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.Read(rx_buf, len, ref count);
			if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
				throw new USBException(status, Catalog.GetString("Cannot read data from USB device."));
			
			data = new byte[count];
			Array.Copy(rx_buf, data, count);

			return count;
		}
		
		public uint Read(out byte[] data)
		{
			List<byte[]> buffers = new List<byte[]>();
			uint count = 0;
			
			do 
			{
				byte[] rx_buf = new byte[Config.ReadBufferSize];
				
				FTD2XX4libusb.FT_STATUS status = this.ftdi4libusb.Read(rx_buf, Config.ReadBufferSize, ref count);
				if (status != FTD2XX4libusb.FT_STATUS.FT_OK)
					throw new USBException(status, Catalog.GetString("Cannot read data from USB device."));
				
				buffers.Add(rx_buf);
			}
			while (count == Config.ReadBufferSize);
			
			data = new byte[Config.ReadBufferSize * buffers.Count];
			
			int offset = 0;
			foreach (byte[] buf in buffers)
			{
				Array.Copy(buf, 0, data, offset, buf.Length);
				offset += buf.Length;
			}
			return (uint)data.Length;
		}

		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)
		{
			uint count = 0;
			FTD2XX4libusb.FT_STATUS status = FTD2XX4libusb.FT_STATUS.FT_NOT_SUPPORTED;

//			for (int ix=0; ix < 5; ++ix)
//			{
				status = this.ftdi4libusb.Write(data, data.Length, ref count);
				if (status == FTD2XX4libusb.FT_STATUS.FT_OK && count == data.Length)
					return count;
//
//				Console.WriteLine("Warning: USB.Write() failed. Try again after 5s delay ({0}).", ix+1);
//				Thread.Sleep(5000);
//			}

			throw new USBException(status, Catalog.GetString("Cannot write data to USB device."));
		}
		
		public DriverType DriverType
		{
			get
			{
				return DriverType.LibUSB;
			}
		}
	}
}
