// Created with MonoDevelop
//
//    Copyright (C) 2023 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 Hiperscan.Common;
using LibUsbDotNet.Main;

namespace Hiperscan.SGS.SerialInterface.USB.DeviceFinder
{
    public delegate void UsbEventHandler(DeviceInfo dev_info);

    public class UsbDeviceFinder : IDisposable
    {
        public const string NO_SERIAL_NUMBER = "NOID";

        public event UsbEventHandler Added = null;
        public event UsbEventHandler Removed = null;

        private UsbDeviceFinder(IEnumerable<SerialInterfaceConfig> configs)
        {
            this.DebugWriter = new DebugWriter
            {
                Debug = Environment.GetEnvironmentVariable(Env.HCL.DEBUG) == "1",
                WriteShortException = true
            };

            this.Configs = configs;
            this.KnownSerials = new Dictionary<string, bool>();
            System.Threading.Tasks.Task.Run(this.Update);
        }

        private void Update()
        {
            this.Stop = false;

            for (; ; )
            {
                if (this.Stop)
                    break;

                System.Threading.Thread.Sleep(2000);
                try
                {
                    lock (UsbDeviceFinder.InstanceSync)
                    {
                        if (this.IsDisposed == false)
                            this.UpdateUsbDevices();
                    }
                }
                catch (Exception ex)
                {
                    this.DebugWriter.WriteLine("Usb device finder cannot refresh usb events:{0}", ex);
                }
            }
        }

        private void UpdateUsbDevices()
        {
            List<string> serials = new List<string>(this.KnownSerials.Keys);

            foreach (string serial in serials)
                this.KnownSerials[serial] = false;

            UsbRegDeviceList usb_list = LibUsbDotNet.UsbDevice.AllDevices;
            if (usb_list != null && usb_list.Count > 0)
            {
                foreach (UsbRegistry usb_reg in usb_list)
                {
                    try
                    {
                        string serial = LibUsbDevice.GetSerialNumber(usb_reg);
                        if (String.IsNullOrEmpty(serial))
                            continue;

                        if (this.KnownSerials.ContainsKey(serial))
                        {
                            this.KnownSerials[serial] = true;
                            continue;
                        }

                        UsbDevice usb_dev = new UsbDevice();
                        usb_dev.Vid = (ushort)usb_reg.Vid;
                        usb_dev.Pid = (ushort)usb_reg.Pid;
                        usb_dev.DeviceId = serial.GetHashCode();
                        usb_dev.Serial = serial;

                        this.KnownSerials.Add(serial, true);
                        this.AddDevice(usb_dev);
                    }
                    catch (Exception)
                    {
                        continue;
                    }
                }
            }

            foreach (string serial in serials)
            {
                if (this.KnownSerials[serial] == false)
                {
                    this.KnownSerials.Remove(serial);
                    UsbDevice usb_dev = new UsbDevice();
                    usb_dev.DeviceId = serial.GetHashCode();
                    usb_dev.Serial = serial;
                    this.RemoveDevice(usb_dev);
                }
            }
        }

        private void AddDevice(UsbDevice usb_dev)
        {
            try
            {
                DeviceInfo device_info = null;

                lock (this.EventSync)
                {
                    this.DebugWriter.WriteLine("\n======================");
                    this.DebugWriter.WriteLine("Adding Device ...");

                    device_info = this.ConvertToDeviceInfo(usb_dev);

                    this.DebugWriter.WriteLine(device_info.ToString());

                    if (this.RegisteredDevices.ContainsKey(usb_dev.DeviceId))
                    {
                        this.DebugWriter.WriteLine("Cannot add device, because it already exists.");
                        return;
                    }

                    if (this.IsValidDevice(device_info) == false)
                        return;

                    this.DebugWriter.WriteLine("Add device with serial {0} (device ID {1})",
                                               device_info.SerialNumber,
                                               usb_dev.DeviceId);


                    this.RegisteredDevices.Add(usb_dev.DeviceId, device_info);
                }

                this.Added?.Invoke(device_info);
            }
            catch(Exception ex)
            {
                this.DebugWriter.WriteLine("Usb device finder cannot add device: {0}", ex);
            }
        }

        private void RemoveDevice(UsbDevice usb_dev)
        {
            try
            {
                if (this.IsDisposed)
                    return;

                DeviceInfo device_info = null;

                lock (this.EventSync)
                {
                    this.DebugWriter.WriteLine("\n======================");
                    this.DebugWriter.WriteLine("Removing Device ...");

                    if (this.RegisteredDevices.ContainsKey(usb_dev.DeviceId) == false)
                    {
                        this.DebugWriter.WriteLine(usb_dev.ToString());
                        this.DebugWriter.WriteLine("Cannot remove device, because it doesn't exist.");
                        return;
                    }

                    device_info = this.RegisteredDevices[usb_dev.DeviceId];

                    this.DebugWriter.WriteLine("Remove device with serial {0} (device ID {1})", 
                                               device_info.SerialNumber,
                                               usb_dev.DeviceId);

                    this.RegisteredDevices.Remove(usb_dev.DeviceId);
                }

                this.Removed?.Invoke(device_info);
            }
            catch(Exception ex)
            {
                this.DebugWriter.WriteLine("Usb device finder cannot remove device: {0}", ex);
            }
        }

        private DeviceInfo ConvertToDeviceInfo(UsbDevice unit)
        {
            string serial = unit.Serial;

            if (string.IsNullOrEmpty(serial)&& this.AcceptAllVendorAndProductIds)
            {
                serial = string.Format("{0}{1:D4}",
                                     UsbDeviceFinder.NO_SERIAL_NUMBER,
                                     UsbDeviceFinder.SerialNumberCount++);
            }

            DeviceInfo dev_info = new DeviceInfo(unit.VendorName,
                                                 serial,
                                                 unit.ProductName,
                                                 unit.Vid,
                                                 unit.Pid);

            return dev_info;
        }

        private bool IsValidDevice(DeviceInfo dev_info)
        {
            if (string.IsNullOrEmpty(dev_info.SerialNumber))
            {
                DebugWriter.WriteLine("Is valid device: No Serial");
                return false;
            }

            if (this.Configs == null)
                return true;

            if (this.Configs.Any(c => c.ProductId == dev_info.ProductId &&
                                 c.VendorId == dev_info.VendorId) == false)
            {
                DebugWriter.WriteLine("Is valid device: No valid config");
                return false;
            }

            return true;
        }

        private bool ContainsVendorAndProductId(uint vendor_id, uint product_id)
        {
            if (this.AcceptAllVendorAndProductIds)
                return true;

            return this.Configs.Any(c => c.ProductId == product_id && c.VendorId == vendor_id);
        }

        public virtual void Dispose()
        {
            lock (UsbDeviceFinder.InstanceSync)
            {
                if (this.IsDisposed)
                    return;

                this.Stop = true;
                this.IsDisposed = true;

                UsbDeviceFinder.CurrentInstance = null;
            }
        }

        public static UsbDeviceFinder GetInstance(IEnumerable<SerialInterfaceConfig> configs)
        {
            lock (UsbDeviceFinder.InstanceSync)
            {
                if (UsbDeviceFinder.CurrentInstance != null)
                    return UsbDeviceFinder.CurrentInstance;

                UsbDeviceFinder.CurrentInstance = new UsbDeviceFinder(configs);

                return UsbDeviceFinder.CurrentInstance;
            }
        }

        public bool AcceptAllVendorAndProductIds => this.Configs == null;

        private DebugWriter DebugWriter { get; set; }
        private bool IsDisposed { get; set; } = false;

        private object EventSync { get; set; } = new object();
        private Dictionary<int, DeviceInfo> RegisteredDevices { get; set; } = new Dictionary<int, DeviceInfo>();
        private IEnumerable<SerialInterfaceConfig> Configs { get; set; }

        private static object InstanceSync { get; set; } = new object();
        private static UsbDeviceFinder CurrentInstance { get; set; }

        private static int SerialNumberCount { get; set; } = 0;
        public static bool AllowNoSerialNumbers { get; set; } = Environment.GetEnvironmentVariable(Env.HCL.ALLOW_NO_SERIAL_NUMBR) == "1";

        private Dictionary<string, bool> KnownSerials { get; set; }
        private bool Stop { get; set; } = false;
    }
}
