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

using Hiperscan.SGS.CalibrationDevice;
using Hiperscan.SGS.Common;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.SGS.SerialInterface.USB;
using Hiperscan.SGS.Sgs1900;
using Hiperscan.Unix;
using LibUsbDotNet;
using LibUsbDotNet.Main;
using Version = Hiperscan.SGS.Common.Version;


namespace Hiperscan.SGS
{
    public class DeviceList
    {
        public const uint VENDOR_ID_FTDI = 0x0403;
        public const uint VENDOR_ID_HIPERSCAN = 0x3111;

        public const uint PRODUCT_ID_SGS1900 = 0x9C70;
        public const uint PRODUCT_ID_SGS1900B = 0x9C72;
        public const uint PRODUCT_ID_SGS1900C = 0x9C73;
        public const uint PRODUCT_ID_SGSNT = 0x0000;
        public const uint PRODUCT_ID_CALIBRATION = 0x0001;
        public const uint PRODUCT_ID_APOIDENT    = 0x0027;
        public const uint PRODUCT_ID_ES20_OEM = 0x0028;
        public const uint PRODUCT_ID_APOIDENT_ES20 = 0x0029;

        public static readonly SerialInterfaceConfig[] DEFAULT_USB_CONFIGS =
        {
            new SerialInterfaceConfig(VENDOR_ID_FTDI,      PRODUCT_ID_SGS1900,     SerialInterfaceConfig.BackendType.FT245BL, DeviceType.Sgs1900),
            new SerialInterfaceConfig(VENDOR_ID_HIPERSCAN, PRODUCT_ID_APOIDENT,    SerialInterfaceConfig.BackendType.FT245BL, DeviceType.ApoIdent),
            new SerialInterfaceConfig(VENDOR_ID_FTDI,      PRODUCT_ID_SGS1900B,    SerialInterfaceConfig.BackendType.FT245BL, DeviceType.PTIM100),
            new SerialInterfaceConfig(VENDOR_ID_FTDI,      PRODUCT_ID_SGS1900C,    SerialInterfaceConfig.BackendType.FT245BL, DeviceType.GerberScan),
            new SerialInterfaceConfig(VENDOR_ID_HIPERSCAN, PRODUCT_ID_SGSNT,       SerialInterfaceConfig.BackendType.CP2130,  DeviceType.SgsNt),
            new SerialInterfaceConfig(VENDOR_ID_HIPERSCAN, PRODUCT_ID_CALIBRATION,   SerialInterfaceConfig.BackendType.FT245BL, DeviceType.CalibrationDevice),
            new SerialInterfaceConfig(VENDOR_ID_HIPERSCAN, PRODUCT_ID_ES20_OEM, SerialInterfaceConfig.BackendType.STM32, DeviceType.ES20),
            new SerialInterfaceConfig(VENDOR_ID_HIPERSCAN, PRODUCT_ID_APOIDENT_ES20, SerialInterfaceConfig.BackendType.STM32, DeviceType.ApoIdent)
        };

        private readonly FT245BL ft245bl;
        private readonly CP2130 cp2130;
        private readonly STM32 stm32;
        private readonly Dictionary<string,RemoteSerialInterfaceClient> remote_usbs;
        private readonly Dictionary<string,IDevice> list; 
        private readonly List<string> blacklist;
        private bool disable_timeout = false;
        private bool activate_queues = false;

        private readonly 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(IDevice dev);
        public event DeviceStateChangedHandler DeviceStateChanged;

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

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


        public DeviceList(IEnumerable<SerialInterfaceConfig> usb_configs)
        {
            try
            {
                this.ft245bl = new FT245BL(usb_configs.Where(c => c.Backend == SerialInterfaceConfig.BackendType.FT245BL));
            }
            catch (DllNotFoundException)
            {
                this.ft245bl = null;
            }

            try
            {
                this.cp2130 = new CP2130(usb_configs.Where(c => c.Backend == SerialInterfaceConfig.BackendType.CP2130));
            }
            catch (DllNotFoundException)
            {
                this.cp2130 = null;
            }

            try
            {
                this.stm32 = new STM32(usb_configs.Where(c => c.Backend == SerialInterfaceConfig.BackendType.STM32));
            }
            catch (DllNotFoundException)
            {
                this.stm32 = null;
            }

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

        public DeviceList(uint vendor_id, uint product_id, SerialInterfaceConfig.BackendType usb_backend)
            : this(new SerialInterfaceConfig[] { DeviceList.GetUsbConfig(vendor_id, product_id, usb_backend) })
        {
        }

        public DeviceList(uint product_id) : this(VENDOR_ID_FTDI, product_id, SerialInterfaceConfig.BackendType.FT245BL)
        {
        }

        public DeviceList() : this(DEFAULT_USB_CONFIGS)
        {
        }

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

            Thread thread = new Thread(new ThreadStart(this.__DoRunAutoUpdate))
            {
                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)
        {
            RemoteSerialInterfaceClient remote_usb = new RemoteSerialInterfaceClient(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(IDevice device)
        {
            lock (this)
            {
                this.DisconnectAndRemoveDevice(device.Info.SerialNumber, true);
            }
        }

        public bool Update()
        {
            lock (this)
            {
                try
                {
                    List<string> added_devices = new List<string>();
                    List<string> blacklisted_devices = new List<string>();
                    List<string> unblacklisted_devices = new List<string>();

                var removed_devices = new Dictionary<string,string>();
                
                bool error = false;
                var infos = new List<DeviceInfo>();

                // every call to UsbDevice.AllDevices initiates Setup requqst to all
                // connected USB devices and generates traffic on USB control endpoint
                // so do it only once here for all USB devices
                UsbRegDeviceList usb_regs = UsbDevice.AllDevices;

                try
                {
                    if (this.ft245bl != null)
                        infos.AddRange(this.ft245bl.GetDeviceList(usb_regs));
                }
                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(usb_regs));
                }
                catch (CP2130.UsbException ex)
                {
                    Console.WriteLine("Cannot get current CP2130 USB device list: {0}", ex.Message);
                    error = true;
                }
                
                try
                {
                    if (this.stm32 != null)
                        infos.AddRange(this.stm32.GetDeviceList(usb_regs));
                }
                catch (STM32.UsbException ex)
                {
                    Console.WriteLine("Cannot get current FT23x USB device list: {0}", ex.Message);
                    error = true;
                }


                if (error)
                    return true;

                this.UpdateAdditionalDeviceTypes(infos);

                Dictionary<string,IDevice> current_list = new Dictionary<string,IDevice>(this.list);
                List<string> current_blacklist = new List<string>(this.blacklist);
                    

                    foreach (DeviceInfo info in infos)
                    {
                        if (info.DeviceType == DeviceType.Sgs1900 && info.Description.Contains("Microspectrometer") == false)
                            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.IsRemote)
                                {
                                    version_info = this.remote_usbs[info.SerialNumber].VersionInfo;
                                }
                                else
                                {
                                    switch (info.UsbBackend)
                                    {

                                        case SerialInterfaceConfig.BackendType.FT245BL:
                                            version_info = FT245BL.GetVersionInfo(info, false);
                                            break;

                                        case SerialInterfaceConfig.BackendType.CP2130:
                                            version_info = CP2130.GetVersionInfo(info, false);
                                            break;

                                        case SerialInterfaceConfig.BackendType.STM32:
                                            version_info = STM32.GetVersionInfo(info, false);
                                            break;

                                        default:
                                            throw new NotSupportedException(Catalog.GetString("Unknown device type."));  
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                if (this.list.ContainsKey(info.SerialNumber))
                                    continue;

                                if (this.blacklist.Contains(info.SerialNumber) == false)
                                {
                                    Console.WriteLine("Adding device to blacklist: {0} ({1})",
                                                      info.SerialNumber,
                                                      ex.Message);
                                    this.blacklist.Add(info.SerialNumber);
                                    blacklisted_devices.Add(info.SerialNumber);
                                }
                                continue;
                            }

                            IDevice 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.v199_21:
                                dev = new CalibrationDevice0_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.v1_0:
                            case (ushort)Version.VersionId.v201_0:
                                dev = new SgsNt.Prototype(info, version_info);
                                break;

                            case (ushort)Version.VersionId.v1_1:
                            case (ushort)Version.VersionId.v202_1:
                                dev = new SgsNt.DeviceNt1_1(info, version_info);
                                break;

                            case (ushort)Version.VersionId.v1_2:
                            case (ushort)Version.VersionId.v201_2:
                                dev = new SgsNt.DeviceNt1_2(info, version_info);
                                break;

                            default:
                            if (this.blacklist.Contains(info.SerialNumber) == false)
                                {
                                    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);
                                        Unix.Timeout.Add(0, () =>
                                        {
                                            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;
                            }

                            this.list.Add(info.SerialNumber, dev);
                            added_devices.Add(info.SerialNumber);

                            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);
                        string dev_name = this.DisconnectAndRemoveDevice(key, false);
                        removed_devices.Add(key, dev_name);
                        Console.WriteLine("Current device count: " + this.list.Count);
                    }


                    if (removed_devices.Count > 0 && (this.DeviceRemoved != null))
                        Unix.Timeout.Add(0, () =>
                        {
                            this.DeviceRemoved(this, removed_devices);
                            return false;
                        });

                    if (added_devices.Count > 0 && (this.DeviceAdded != null))
                        Unix.Timeout.Add(0, () =>
                        {
                            this.DeviceAdded(this, added_devices);
                            return false;
                        });

                    if ((added_devices.Any() || removed_devices.Any() || blacklisted_devices.Any() || unblacklisted_devices.Any()) &&
                        this.Changed != null)
                    {
                        Unix.Timeout.Add(0, () =>
                        {
                            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);
                }
            }
        }

        private string DisconnectAndRemoveDevice(string key, bool remove_from_remote_usb)
        {
            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;

            this.list[key].SetState(Spectrometer.State.Disconnected);
            string dev_name = this.list[key].CurrentConfig.Name;

            if (remove_from_remote_usb)
            {
                this.remote_usbs[key].Disconnect();
                this.remote_usbs.Remove(key);
            }
            else
            {
                this.list.Remove(key);
            }

            return dev_name;
        }

        public void ResetBlacklist()
        {
            lock (this)
                this.blacklist.Clear();
        }

        private void StateChangedNotify(IDevice dev)
        {
            this.DeviceStateChanged?.Invoke(dev);
        }

        private void DeviceIdleTaskNotify()
        {
            //this.IdleTask?.Invoke();
        }

        private void NotifyDeviceException(SgsException ex)
        {
            this.DeviceException?.Invoke(ex);
        }

        private void NotifyDeviceWarning(IDevice dev, string message)
        {
            this.DeviceWarning?.Invoke(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)
        {
            lock (this)
                return this.list.Values.Any(d => d.CurrentConfig.Name == name);
        }

        public IDevice GetFirstDevice()
        {
            lock (this)
                return this.list.Values.FirstOrDefault();
        }

        public IDevice GetByName(string name)
        {
            lock (this)
            {
                return this.list.Values.FirstOrDefault(d => d.CurrentConfig.Name == name);
            }
        }

        public bool Contains(IDevice dev)
        {
            lock (this)
                return this.list.ContainsValue(dev);
        }

        private void UpdateAdditionalDeviceTypes(List<DeviceInfo> infos)
        {
            HashSet<string> usb_serials = new HashSet<string>(infos.Select(x => x.SerialNumber));
            HashSet<string> remote_serials = new HashSet<string>(remote_usbs.Select(x => x.Key));

            HashSet<string> remove_serials = new HashSet<string>();
            foreach (string remote_serial in remote_serials)
            {
                if (usb_serials.Contains(remote_serial))
                    remove_serials.Add(remote_serial);
            }

            foreach (string remove_serial in remove_serials)
            {
                infos.Remove(infos.Find(x => x.SerialNumber == remove_serial));
                this.DisconnectAndRemoveDevice(remove_serial, true);
            }

            foreach (RemoteSerialInterfaceClient remote_usb in this.remote_usbs.Values)
                infos.Add(remote_usb.DeviceInfo);
        }

        private static SerialInterfaceConfig GetUsbConfig(uint vendor_id, uint product_id, SerialInterfaceConfig.BackendType usb_backend)
        {
            return DeviceList.DEFAULT_USB_CONFIGS.SingleOrDefault(c => c.VendorId == vendor_id &&
                                                                  c.ProductId == product_id &&
                                                                  c.Backend == usb_backend) ?? throw new NotSupportedException("Unsupported USB device configuration.");
        }

        public IDevice this[string key]
        {
            get
            {
                lock (this)
                {
                    if (this.list.ContainsKey(key))
                        return this.list[key];
                    else
                        return null;
                }
            }
        }

        public Dictionary<string, IDevice>.ValueCollection Values
        {
            get
            {
                lock (this)
                    return this.list.Values;
            }
        }

        public Dictionary<string, IDevice>.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 AllIdleOrDisconnected
        {
            get
            {
                bool result = true;

                lock (this)
                {
                    foreach (IDevice dev in this.list.Values)
                    {
                        Spectrometer.State state = dev.GetState();
                        if (state != Spectrometer.State.Idle &&
                            state != Spectrometer.State.Disconnected &&
                            state != Spectrometer.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 (IDevice 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 bool AutoUpdate
        {
            get { return DeviceList.auto_update; }
        }
    }
}
