// 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 LibFTDI;
//using Hiperscan.FTDI4libusb;


namespace FTD2XX_NET4libusb
{
	public class FTD2XX4libusb
	{	
		public enum FT_STATUS
		{
			FT_OK,
			FT_INVALID_HANDLE,
			FT_DEVICE_NOT_FOUND,
			FT_DEVICE_NOT_OPENED,
			FT_IO_ERROR,
			FT_INSUFFICIENT_RESOURCES,
			FT_INVALID_PARAMETER,
			FT_INVALID_BAUD_RATE,
			FT_DEVICE_NOT_OPENED_FOR_ERASE,
			FT_DEVICE_NOT_OPENED_FOR_WRITE,
			FT_FAILED_TO_WRITE_DEVICE,
			FT_EEPROM_READ_FAILED,
			FT_EEPROM_WRITE_FAILED,
			FT_EEPROM_ERASE_FAILED,
			FT_EEPROM_NOT_PRESENT,
			FT_EEPROM_NOT_PROGRAMMED,
			FT_INVALID_ARGS,
			FT_NOT_SUPPORTED,
		}
		
		public class FT_DEVICE_INFO_NODE
		{
			private string manufacturer;
			private string description;
			private string serial_number;
			private uint vendor_id;
			private uint product_id;
			
			public FT_DEVICE_INFO_NODE(uint vid, uint pid, string mnf, string dsc, string ser)
			{
				this.vendor_id  = vid;
				this.product_id = pid;
				
				this.manufacturer  = mnf;
				this.description   = dsc;
				this.serial_number = ser;
			}
						
			public string Manufacturer { get { return this.manufacturer;  } }
			public string Description  { get { return this.description;   } }
			public string SerialNumber { get { return this.serial_number; } }
			public uint   VendorId     { get { return this.vendor_id;     } }
			public uint   ProductId    { get { return this.product_id;    } }
		}
		
		public class FT_PURGE
		{
			public const uint FT_PURGE_RX = 1;
			public const uint FT_PURGE_TX = 2;
			
			public FT_PURGE()
			{
			}
		}
		
		private FTDI ftdi;
		private volatile bool is_open;
		private uint vendor_id  = 0x0403;
		private uint[] product_ids = new uint[] { 0x6001 };
		private static object global_mutex = new object();
		
		public FTD2XX4libusb()
		{
			this.is_open = false;
			
			try
			{
				lock (global_mutex)
				{
					FTDI.PreloadLibrary();
					FTDI.CheckForLibftdi();
				}
			}
			catch (System.DllNotFoundException ex)
			{
				Console.WriteLine("FTD2XX4libusb: Cannot find libftdi. Throwing DLLNotFoundException");
				throw ex;
			}
		}
		
		public FT_STATUS OpenBySerialNumber(string serial_number)
		{
			lock (global_mutex)
			{
				this.ftdi = null;

				foreach (uint pid in this.product_ids)
				{
					try
					{
						this.ftdi = new FTDI((int)this.vendor_id, (int)pid, null, serial_number);
					}
					catch (Exception)
					{
						continue;
					}
					break;
				}
				if (this.ftdi == null)
					return FT_STATUS.FT_DEVICE_NOT_FOUND;

				this.is_open = true;
				return FT_STATUS.FT_OK;
			}
		}

		public FT_STATUS GetNumberOfDevices(ref uint count)
		{
			lock (global_mutex)
			{
				try
				{
					List<IntPtr> list = new List<IntPtr>();

					foreach (uint pid in this.product_ids)
					{
						list.AddRange(FTDI.GetDeviceList((int)this.vendor_id, (int)pid));
					}

					count = (uint)list.Count;
					return FT_STATUS.FT_OK;
				}
				catch (System.Threading.ThreadAbortException)
				{
					// application exit
					count = 0;
					return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("GetNumberOfDevices(): " + ex.Message);
					count = 0;
					return FT_STATUS.FT_IO_ERROR;
				}
			}
		}

		public FT_STATUS GetDeviceList(FT_DEVICE_INFO_NODE[] list)
		{
			// lock is not neccessary for reading general USB device information
			//lock (global_mutex)
			{
				List<DeviceInfo> infos = new List<DeviceInfo>();
				
				try
				{
					foreach (uint pid in this.product_ids)
					{
						DeviceInfo[] device_infos = FTDI.GetDeviceInfoList((int)this.vendor_id, (int)pid);

						for (int ix=0; ix < device_infos.Length; ++ix)
						{
							list[ix+infos.Count] = new FT_DEVICE_INFO_NODE(vendor_id, pid,
							                                               device_infos[ix].Manufacturer,
							                                               device_infos[ix].Description,
							                                               device_infos[ix].Serial);
						}

						infos.AddRange(device_infos);
					}

					if (infos.Count == 0)
					{
						return FT_STATUS.FT_DEVICE_NOT_FOUND;
					}
					
					return FT_STATUS.FT_OK;
				}
				catch (System.NullReferenceException ex)
				{
					throw ex;
				}
				catch (System.IndexOutOfRangeException ex)
				{
					throw ex;
				}
				catch (System.Threading.ThreadAbortException)
				{
					// application exit
					return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("GetDeviceList(): " + ex.Message);
					return FT_STATUS.FT_IO_ERROR;
				}
			}
		}
		
		public FT_STATUS ResetDevice()
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				try
				{
					this.ftdi.Reset();
					return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("ResetDevice(): " + ex.Message);
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				}
			}
		}

		public FT_STATUS SetTimeouts(uint read_timeout, uint write_timeout)
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				this.ftdi.ReadTimeout  = read_timeout;
				this.ftdi.WriteTimeout = write_timeout;
				return FT_STATUS.FT_OK;
			}
		}
		
		public FT_STATUS SetLatency(byte latency)
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				this.ftdi.LatencyTimer = latency;
				return FT_STATUS.FT_OK;
			}
		}

		public FT_STATUS Read(byte[] data, uint size, ref uint rx_bytes)
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				try
				{
					rx_bytes = (uint)this.ftdi.ReadData(ref data, (int)size);
					if (rx_bytes < 0)
						return FT_STATUS.FT_IO_ERROR;
					else
						return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("Read(): " + ex);
					data = new byte[0];
					return FT_STATUS.FT_IO_ERROR;
				}
			}
		}

		public FT_STATUS Read(out string str, uint size, ref uint rx_bytes)
		{
			lock (this)
			{
				if (!this.is_open)
				{
					str = string.Empty;
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				}
				
				try
				{
					byte[] data = new byte[size];
					rx_bytes = (uint)this.ftdi.ReadData(ref data, (int)size);
					if (rx_bytes < 0)
					{
						str = string.Empty;
						return FT_STATUS.FT_IO_ERROR;
					}
					char[] char_data = new char[data.Length];
					for (int ix=0; ix < data.Length; ++ix)
					{
						char_data[ix] = (char)data[ix];
					}
					str = new string(char_data);
					return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("Read(): " + ex.Message);
					str = string.Empty;
					return FT_STATUS.FT_IO_ERROR;
				}
			}
		}

		public FT_STATUS Write(byte[] data, int size, ref uint tx_bytes)
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				try
				{
					tx_bytes = (uint)this.ftdi.WriteData(data, size);
					if (tx_bytes < 0)
						return FT_STATUS.FT_IO_ERROR;
					else
						return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("Write(): " + ex.Message);
					return FT_STATUS.FT_IO_ERROR;
				}
			}
		}
		
		public FT_STATUS GetRxBytesAvailable(ref uint count)
		{
			// FIXME: no hook for this method in libftdi
			// so return a value which is hopefully greater than
			// the actually available byte count
			count = 10000;
			return FT_STATUS.FT_OK;
		}

		public FT_STATUS Purge(uint purge)
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				try
				{
					if ( (purge & FT_PURGE.FT_PURGE_RX) != 0 &&
					    (purge & FT_PURGE.FT_PURGE_TX) != 0 )
					{
						this.ftdi.PurgeBuffers();
						return FT_STATUS.FT_OK;
					}
					else
					{
						throw new ArgumentException("Purging RX or TX buffer serperately is not yet implemented in the libftdi wrapper.");
					}
				}
				catch (ArgumentException e)
				{
					throw e;
				}
				catch (Exception ex)
				{
					Console.WriteLine("Purge(): " + ex.Message);
					return FT_STATUS.FT_IO_ERROR;
				}
			}
		}			
						
		public FT_STATUS Close()
		{
			lock (this)
			{
				if (!this.is_open)
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				
				try
				{
					ftdi.Close();
					this.is_open = false;
					return FT_STATUS.FT_OK;
				}
				catch (Exception ex)
				{
					Console.WriteLine("Close(): " + ex.Message);
					return FT_STATUS.FT_DEVICE_NOT_OPENED;
				}
			}
		}
		
		public bool IsOpen 
		{
			get 
			{ 
				lock (this)
					return this.is_open; 
			}
		}
		
		public uint VendorId
		{
			get { return this.vendor_id;  }
			set { this.vendor_id = value; }
		}

		public uint[] ProductIds
		{
			get { return this.product_ids;  }
			set { this.product_ids = value; }
		}
	}
}
