// 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.Threading;
using System.Collections.Generic;

using Hiperscan.Common;
using Hiperscan.Unix;


namespace Hiperscan.SGS
{
	
	public class DeviceList
	{
		public static readonly uint DefaultVendorId  = 0x0403;
		public static readonly uint DefaultProductId = 0x9C70;
			
        private FT245BL ft245bl;
        private CP2130  cp2130;
        private Dictionary<string,RemoteUSBClient> remote_usbs;
		private Dictionary<string,Device> list; 
		private List<string> blacklist;
		private bool disable_timeout = false;
		private bool activate_queues = false;

        private Mutex auto_update_mutex = new Mutex();
        internal static volatile bool auto_update = false;
		
		public delegate void DeviceAddedHandler(DeviceList list, List<string> added);
		public event         DeviceAddedHandler DeviceAdded;

		public delegate void DeviceRemovedHandler(DeviceList list, Dictionary<string,string> removed);
		public event         DeviceRemovedHandler DeviceRemoved;

		public delegate void ChangedHandler(DeviceList list);
		public event         ChangedHandler Changed;
		
		public delegate void DeviceStateChangedHandler(Device dev);
		public event         DeviceStateChangedHandler DeviceStateChanged;

		//public delegate void IdleTaskHandler(int thread_id);
		public event         IdleTaskHandler IdleTask;

		public delegate void DeviceExceptionHandler(SGSException ex);
		public event         DeviceExceptionHandler DeviceException;

		public delegate void DeviceWarningHandler(Device dev, string message);
		public event         DeviceWarningHandler DeviceWarning;

		public DeviceList(uint vendor_id, uint[] product_ids)
		{
			try
			{
                this.ft245bl = new FT245BL(vendor_id, product_ids);
			}
			catch (DllNotFoundException)
			{
                this.ft245bl = null;
			}

            try
            {
                this.cp2130 = new CP2130(0x10c4, 0x8bac);
            }
            catch (DllNotFoundException)
            {
                this.cp2130 = null;
            }

			this.list = new Dictionary<string,Device>();
			this.blacklist   = new List<string>();
			this.remote_usbs = new Dictionary<string,RemoteUSBClient>();
		}

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

		public DeviceList(uint product_id) : this(DefaultVendorId, product_id)
		{
		}

		public DeviceList(uint[] product_ids) : this(DefaultVendorId, product_ids)
		{
		}
		
		public DeviceList() : this(DeviceList.DefaultVendorId, DeviceList.DefaultProductId)
		{
		}

		public void StartAutoUpdate()
		{
			this.Update();

			Thread thread = new Thread(new ThreadStart(this.__DoRunAutoUpdate));
			thread.IsBackground = true;
			thread.Start();
		}

        public void StopAutoUpdate()
        {
            lock (this.auto_update_mutex)
                DeviceList.auto_update = false;
        }

		private void __DoRunAutoUpdate()
		{
            DeviceList.auto_update = true;

            while (DeviceList.auto_update)
			{
                lock (this.auto_update_mutex)
                    this.Update();
				Thread.Sleep(1000);
			}

            Console.WriteLine("USB auto update terminated.");
		}
		
		public void AddRemoteDevice(string server, uint port)
		{
			RemoteUSBClient remote_usb = new RemoteUSBClient(server, port);
			lock (this)
			{
				if (this.ContainsSerial(remote_usb.DeviceInfo.SerialNumber))
					throw new Exception(string.Format(Catalog.GetString("A device with serial number {0} is already connected."),
					                                  remote_usb.DeviceInfo.SerialNumber));
			this.remote_usbs.Add(remote_usb.DeviceInfo.SerialNumber, remote_usb);
			}
		}
		
		public void RemoveRemoteDevice(Device device)
		{
			lock (this)
			{
				this.remote_usbs[device.Info.SerialNumber].Disconnect();
				this.remote_usbs.Remove(device.Info.SerialNumber);
			}
		}
		
		public bool Update()
		{
			try
			{
				List<string> added_devices         = new List<string>();
				List<string> blacklisted_devices   = new List<string>();
				List<string> unblacklisted_devices = new List<string>();
				Dictionary<string,string> removed_devices = new Dictionary<string,string>();
				
                bool error = false;
                List<DeviceInfo> infos = new List<DeviceInfo>();

				try
				{
                    if (this.ft245bl != null)
                        infos.AddRange(this.ft245bl.GetDeviceList());
				}
				catch (FT245BL.USBException ex)
				{
					Console.WriteLine("Cannot get current FT245BL USB device list: {0}", ex.Message);
					error = true;
				}
					
                try
                {
                    if (this.cp2130 != null)
                        infos.AddRange(this.cp2130.GetDeviceList());
                }
                catch (FT245BL.USBException ex)
                {
                    Console.WriteLine("Cannot get current CP2130 USB device list: {0}", ex.Message);
                    error = true;
                }

                if (error)
                    return true;

                this.UpdateAdditionalDeviceTypes(infos);

				Dictionary<string,Device> current_list = new Dictionary<string,Device>(this.list);
				List<string> current_blacklist = new List<string>(this.blacklist);
					
				lock (this)
				{
					foreach (DeviceInfo info in infos)
					{
                        if (info.DeviceType == DeviceType.SGS && !info.Description.Contains("Microspectrometer"))
							continue;

						if (current_blacklist.Contains(info.SerialNumber))
						{
							current_blacklist.Remove(info.SerialNumber);
							continue;
						}

						if (current_list.ContainsKey(info.SerialNumber))
						{
							current_list.Remove(info.SerialNumber);
						}
						else
						{
							VersionInfo version_info;
							try
							{
								if (info.DeviceType == DeviceType.NSeries)
								{
									version_info = new VersionInfo(new Version((ushort)Version.VersionId.NSeries));
								}
                                else if (info.DeviceType == DeviceType.SGSNT)
                                {
                                    version_info = new VersionInfo(new Version((ushort)Version.VersionId.SgsNtPrototype));
                                }
								else if (info.IsRemote)
								{
									version_info = this.remote_usbs[info.SerialNumber].VersionInfo;
								}
								else
								{
									version_info = Device.GetVersionInfo(info, false);
								}
							}
							catch (Exception ex)
							{
								if (this.list.ContainsKey(info.SerialNumber))
									continue;
								
								if (!this.blacklist.Contains(info.SerialNumber))
								{
									Console.WriteLine("Adding device to blacklist: {0} ({1})",
									                  info.SerialNumber,
									                  ex.Message);
									this.blacklist.Add(info.SerialNumber);
									blacklisted_devices.Add(info.SerialNumber);
//									if (this.DeviceException != null)
//										this.DeviceException(new SGSException(ex.Message, null));
								}
								continue;
							}

							Device dev;

							switch (version_info.ProtocolVersion.Id)
							{
								
							case (ushort)Version.VersionId.v0_0:
								dev = new Device0_0(info, version_info);
								break;
								
							case (ushort)Version.VersionId.v0_10:
								dev = new Device0_10(info, version_info);
								break;
								
							case (ushort)Version.VersionId.v0_11:
								dev = new Device0_11(info, version_info);
								break;
								
							case (ushort)Version.VersionId.v0_12:
								dev = new Device0_12(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_13:
								dev = new Device0_13(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_14:
								dev = new Device0_14(info, version_info);
								break;
								
							case (ushort)Version.VersionId.v0_15:
								dev = new Device0_15(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_16:
								dev = new Device0_16(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_17:
								dev = new Device0_17(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_18:
								dev = new Device0_18(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_19:
								dev = new Device0_19(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_20:
								dev = new Device0_20(info, version_info);
								break;

							case (ushort)Version.VersionId.v0_21:
								dev = new Device0_21(info, version_info);
								break;

                            case (ushort)Version.VersionId.v200_22:
                                dev = new Device0_22(info, version_info);
                                break;

                            case (ushort)Version.VersionId.v200_255:
                                dev = new Device0_255(info, version_info);
                                break;

                            case (ushort)Version.VersionId.NSeries:
								dev = new Hiperscan.SpectralEngines.NSeries(info, version_info);
								break;
                                
                            case (ushort)Version.VersionId.SgsNtPrototype:
                                    dev = new SgsNtPrototype(info, version_info);
                                    break;

							default:
								if (!this.blacklist.Contains(info.SerialNumber))
								{
									Console.WriteLine("DeviceList.Update(): Adding to blacklist: " + info.SerialNumber);
									this.blacklist.Add(info.SerialNumber);
									blacklisted_devices.Add(info.SerialNumber);
									if (this.DeviceException != null)
									{
										string msg = string.Format(Catalog.GetString("Cannot open device with serial number {0}: " +
										                                             "Unknown protocol version {1}."),
										                           info.SerialNumber, version_info.ProtocolVersion.ToString()
										                           );
										Hiperscan.Unix.Timeout.Add(0, delegate() {
											this.DeviceException(new SGSException(msg, null));
											return false;
										});

										Console.WriteLine("Threw device exception...");
									}
								}
								continue;
								
							}
							
							dev.SpectrumQueue.IsActive = this.activate_queues;
							
							dev.StateChanged += this.StateChangedNotify;
							dev.IdleTask     += this.DeviceIdleTaskNotify;
							dev.NewException += this.NotifyDeviceException;
							dev.NewWarning   += this.NotifyDeviceWarning;

							if (this.disable_timeout)
							{
								dev.CurrentConfig.AutoTimeout = false;
								dev.CurrentConfig.Timeout = 0;
							}
							
							if (!this.list.ContainsKey(info.SerialNumber))
							{
								this.list.Add(info.SerialNumber, dev);
								added_devices.Add(info.SerialNumber);
							}
							else
							{
								// should never happen!
							}
							
							Console.WriteLine("DeviceList.Update(): Adding new device " + info.SerialNumber);
							Console.WriteLine("Current device count: " + this.list.Count);
						}
					}
				
					foreach (string key in current_blacklist)
					{
						Console.WriteLine("DeviceList.Update(): Removing from blacklist: " + key);
						this.blacklist.Remove(key);
						unblacklisted_devices.Add(key);
					}
				
					foreach (string key in current_list.Keys)
					{
						Console.WriteLine("DeviceList.Update(): Removing device " + key);
                        this.list[key].SetState(Device.State.Disconnected);
						this.list[key].Close();
						this.list[key].RemoveNotify();
						this.list[key].StateChanged -= this.StateChangedNotify;
						this.list[key].IdleTask     -= this.DeviceIdleTaskNotify;
						this.list[key].NewException -= this.NotifyDeviceException;
						this.list[key].NewWarning   -= this.NotifyDeviceWarning;
						removed_devices.Add(key, this.list[key].CurrentConfig.Name);
						this.list.Remove(key);
						Console.WriteLine("Current device count: " + this.list.Count);
					}
				}
				
				if (removed_devices.Count > 0 && (this.DeviceRemoved != null))
					Hiperscan.Unix.Timeout.Add(0, delegate {
						this.DeviceRemoved(this, removed_devices);
						return false;
					});

				
				if (added_devices.Count > 0 && (this.DeviceAdded   != null))
					Hiperscan.Unix.Timeout.Add(0, delegate {
						this.DeviceAdded(this, added_devices);
						return false;
					});
				
				if ((added_devices.Count > 0       || removed_devices.Count > 0 || 
				     blacklisted_devices.Count > 0 || unblacklisted_devices.Count > 0) && (this.Changed != null))
					Hiperscan.Unix.Timeout.Add(0, delegate {
						this.Changed(this);
						return false;
					});

				return true;
			}
            catch (Exception ex) when (ex is ObjectDisposedException || ex is ThreadAbortException)
			{
				// application exit
				return true;
			}
			catch (Exception ex)
			{
				Console.WriteLine("Catched exception: " + ex);
				throw new Exception(Catalog.GetString("Unhandled exception in DeviceList.Update():") + " " + ex);
			}
		}
		
		public void ResetBlacklist()
		{
			lock (this)
				this.blacklist.Clear();
		}
		
		private void StateChangedNotify(Device dev)
		{
			if (this.DeviceStateChanged != null)
				this.DeviceStateChanged(dev);
		}
		
		private void DeviceIdleTaskNotify()
		{
//			if (this.IdleTask != null)
//				this.IdleTask();
		}
		
		private void NotifyDeviceException(SGSException ex)
		{
			if (this.DeviceException != null)
				this.DeviceException(ex);
		}

		private void NotifyDeviceWarning(Device dev, string message)
		{
			if (this.DeviceWarning != null)
				this.DeviceWarning(dev, message);
		}
				
		public bool Contains(string key)
		{
			lock (this)
				return this.list.ContainsKey(key);
		}
		
		public bool ContainsSerial(string serial)
		{
			lock (this)
				return this.list.ContainsKey(serial);
		}
		
		public bool ContainsName(string name)
		{
			bool found = false;
			
			lock (this)
			{
				foreach (Device dev in this.list.Values)
				{
					if (dev.CurrentConfig.Name == name)
					{
						found = true;
						break;
					}
				}
			}
			return found;
		}
		
		public Device GetFirstDevice()
		{
			lock (this)
			{
				foreach (Device dev in this.list.Values)
				{
					return dev;
				}
				return null;
			}
		}
		
		public Device GetByName(string name)
		{
			lock (this)
			{
				foreach (Device dev in this.list.Values)
				{
					if (dev.CurrentConfig.Name == name)
						return dev;
				}
			}
			return null;
		}
		
		public bool Contains(Device dev)
		{
			lock (this)
				return this.list.ContainsValue(dev);
		}

		private void UpdateAdditionalDeviceTypes(List<DeviceInfo> infos)
		{
			foreach (RemoteUSBClient remote_usb in this.remote_usbs.Values)
			{
				infos.Add(remote_usb.DeviceInfo);
			}

            if (Environment.GetEnvironmentVariable(Env.HCL.SPECTRAL_ENGINES_SUPPORT) == "1" &&
				Environment.OSVersion.Platform != PlatformID.Win32NT &&
			    Environment.OSVersion.Platform != PlatformID.Win32S  &&
			    Environment.OSVersion.Platform != PlatformID.Win32Windows &&
			    Environment.OSVersion.Platform != PlatformID.WinCE)
			{
				if (System.IO.File.Exists("/dev/ttyACM0"))
					infos.Add(new DeviceInfo("Spectral Engines", "ttyACM0", 0x1fc9, 0x0011, DeviceType.NSeries));
				if (System.IO.File.Exists("/dev/ttyACM1"))
					infos.Add(new DeviceInfo("Spectral Engines", "ttyACM1", 0x1fc9, 0x0011, DeviceType.NSeries));
				if (System.IO.File.Exists("/dev/ttyACM2"))
					infos.Add(new DeviceInfo("Spectral Engines", "ttyACM2", 0x1fc9, 0x0011, DeviceType.NSeries));
			}
		}
		
		public Device this[string key]
		{
			get
			{
				lock (this)
				{
					if (this.list.ContainsKey(key))
						return this.list[key];
					else
						return null;
				}
			}
		}
		
		public Dictionary<string,Device>.ValueCollection Values
		{
			get
			{
				lock (this)
					return this.list.Values;
			}
		}

		public Dictionary<string,Device>.KeyCollection Keys
		{
			get
			{
				lock (this)
					return this.list.Keys;
			}
		}
		
		public int Count
		{
			get
			{
				lock (this)
					return this.list.Count;
			}
		}
		
		public bool DisableTimeout
		{
			set { this.disable_timeout = value; }
		}
		
//		public bool HasBackgroundSpectrum
//		{
//			get
//			{
//				bool result = false;
//				
//				foreach (Device dev in this.list.Values)
//					result = dev.CurrentConfig.HasBackgroundSpectrum;
//				
//				return result;
//			}
//		}
		
		public bool AllIdleOrDisconnected
		{
			get
			{
				bool result = true;
				
				lock (this)
				{
					foreach (Device dev in this.list.Values)
					{
						Device.State state = dev.GetState();
						if (state != Device.State.Idle && 
						    state != Device.State.Disconnected &&
						    state != Device.State.Unknown)
						{
							result = false;
							break;
						}
					}
				}
				return result;
			}
		}
		
		public DriverType DriverType
		{
			get 
			{ 
                if (this.ft245bl != null)
                    return this.ft245bl.DriverType; 
                else if (this.cp2130 != null)
                    return this.cp2130.DriverType;
                else
					return DriverType.None;
			}
		}
		
		public bool ActivateQueues
		{
			get 
			{ 
				lock (this)
					return this.activate_queues;
			}
			set
			{
				lock (this)
				{
					foreach (Device dev in this.list.Values)
					{
						dev.SpectrumQueue.IsActive = value;
					}
					this.activate_queues = value;
				}
			}
		}
		
		public List<string> Blacklist
		{
			get 
			{
				lock (this)
					return new List<string>(this.blacklist); 
			}
		}

		public static string GetDeviceNameByProductId(uint pid)
		{
			switch (pid)
			{

			case 0x9C70:
				return "SGS1900";

			case 0x9C72:
				return "PT-IM100";

			case 0x9C73:
				return "GerberScan";

			case 0x0011:
				return "NSeries";

            case 0x8bac:
                return "SGS-NT";

			default:
				return Catalog.GetString("Unknown");

			}
		}

		public bool AutoUpdate
		{
            get { return DeviceList.auto_update; }
		}
	}
}
