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

using Hiperscan.SGS.CalibrationDevice;
using Hiperscan.SGS.Common;
using Hiperscan.SGS.SerialInterface;
using Hiperscan.SGS.SerialInterface.USB;
using Hiperscan.SGS.SerialInterface.USB.DeviceFinder;
using Hiperscan.SGS.Sgs1900;
using Hiperscan.Unix;

using OperatingSystem = Hiperscan.Common.OperatingSystem;
using Version = Hiperscan.SGS.Common.Version;

namespace Hiperscan.SGS
{
    public partial class DeviceList
    {
        public delegate void DeviceAddedHandler(DeviceList list, string serial);
        public event DeviceAddedHandler DeviceAdded;

        public delegate void DeviceRemovedHandler(DeviceList list, string serial, string device_name);
        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 void ResetBlacklist()
        {
            //TODO
            /*
            lock (this)
                this.Blacklist.Clear();
                */
        }

        private DeviceList(IEnumerable<SerialInterfaceConfig> usb_configs, bool only_remote_devices)
        {
            OperatingSystem.PreventUsbPowerDown();

            this.ConnectOnlyToRemoteDevices = only_remote_devices;

            this.DeviceFinder = UsbDeviceFinder.GetInstance(usb_configs);
            this.DeviceFinder.Added += this.OnDeviceAdded;
            this.DeviceFinder.Removed += this.OnDeviceRemoved;
        }

        private void OnDeviceAdded(DeviceInfo device_info)
        {
            lock(this)
            {
                try
                {
                    if (this.IsDisposed)
                        return;

                    if (this.ConnectOnlyToRemoteDevices && device_info.IsRemote == false)
                        return;

                    if (device_info.IsRemote == false)
                        this.MoveRemoteDeviceToUsb(device_info);

                    VersionInfo version_info = this.GetVersionInfo(device_info);
                    if (version_info == null)
                        return;

                    IDevice dev = this.InitDevice(device_info, version_info);
                    if (dev == null)
                        return;

                    this.Devices.Add(device_info.SerialNumber, dev);

                    this.DeviceAdded?.Invoke(this, device_info.SerialNumber);
                    this.Changed?.Invoke(this);

                    Console.WriteLine("DeviceList.Update(): Adding new device " + device_info.SerialNumber);
                    Console.WriteLine("Current device count: " + this.Devices.Count);
                }
                catch (Exception ex)
                {
                    this.HandleExceptions(ex);
                }
            }
        }

        private VersionInfo GetVersionInfo(DeviceInfo device_info)
        {
            try
            {
                if (device_info.IsRemote)
                    return device_info.RemoteSerialInterfaceClient.VersionInfo;

                switch (device_info.UsbBackend)
                {
                    case SerialInterfaceConfig.BackendType.FT245BL:
                        return FT245BL.GetVersionInfo(device_info, false);

                    case SerialInterfaceConfig.BackendType.CP2130:
                        return CP2130.GetVersionInfo(device_info, false);

                    case SerialInterfaceConfig.BackendType.STM32:
                        return STM32.GetVersionInfo(device_info, false);

                    case SerialInterfaceConfig.BackendType.Raspian:
                        return RaspianCom.GetVersionInfo(device_info, false);

                    default:
                        throw new NotSupportedException(Catalog.GetString("Unknown device type."));
                }
            }
            catch (Exception ex)
            {
                string message = String.Format("Adding device to blacklist: {0} ({1}", device_info.SerialNumber, ex.Message);
                this.OnDeviceBlackListed(device_info.SerialNumber, message, false);
                return null;
            }
        }

        private IDevice InitDevice(DeviceInfo device_info, VersionInfo version_info)
        {
            IDevice dev = null;

            switch (version_info.ProtocolVersion.Id)
            {

                case (ushort)Version.VersionId.v0_0:
                    dev = new Device0_0(device_info, version_info);
                    break;

                case (ushort)Version.VersionId.v0_10:
                    dev = new Device0_10(device_info, version_info);
                    break;

                case (ushort)Version.VersionId.v0_11:
                    dev = new Device0_11(device_info, version_info);
                    break;

                case (ushort)Version.VersionId.v0_12:
                    dev = new Device0_12(device_info, version_info);
                    break;

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

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

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

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

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

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

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

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

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

                case (ushort)Version.VersionId.v199_21:
                    dev = new CalibrationDevice0_21(device_info, version_info);
                    break;

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

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

                case (ushort)Version.VersionId.v1_0:
                case (ushort)Version.VersionId.v201_0:
                    dev = new SgsNt.Prototype(device_info, version_info);
                    break;

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

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

                case (ushort)Version.VersionId.v1_5:
                    dev = new SgsNt.DeviceNt1_5(device_info, version_info);
                    break;

                case (ushort)Version.VersionId.v1_6:
                    dev = new SgsNt.DeviceNt1_6(device_info, version_info);
                    break;


                default:
                    string message = string.Format(Catalog.GetString("Cannot open device with the serial number {0}: Unknown protocol version {1}."),
                                                   device_info.SerialNumber,
                                                   version_info.ProtocolVersion);

                    this.OnDeviceBlackListed(device_info.SerialNumber,message, true);
                    return null;
            }

            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.TimeoutMs = 0;
            }

            return dev;
        }

        private void OnDeviceRemoved(DeviceInfo device_info)
        {
            lock (this)
            {
                try
                {
                    if (this.IsDisposed)
                        return;

                    if (this.blacklist.Contains(device_info.SerialNumber))
                    {
                        this.blacklist.Remove(device_info.SerialNumber);
                        this.Changed?.Invoke(this);
                        return;
                    }

                    if (this.Devices.ContainsKey(device_info.SerialNumber))
                    {
                        string device_name = this.DisconnectAndRemoveDevice(device_info.SerialNumber);
                        this.DeviceRemoved?.Invoke(this, device_info.SerialNumber, device_name);
                        this.Changed?.Invoke(this);
                    }
                }
                catch (Exception ex)
                {
                    this.HandleExceptions(ex);
                }
            }
        }

        private string DisconnectAndRemoveDevice(string serial)
        {
            IDevice device = this.Devices[serial];
            string dev_name = device.CurrentConfig.Name;

            device.SetState(Spectrometer.State.Disconnected);

            device.Close();
            device.RemoveNotify();
            device.StateChanged -= this.StateChangedNotify;
            device.IdleTask -= this.DeviceIdleTaskNotify;
            device.NewException -= this.NotifyDeviceException;
            device.NewWarning -= this.NotifyDeviceWarning;
            
            try
            {
                if (device.Info.IsRemote)
                    device.Info.RemoteSerialInterfaceClient.Disconnect();
            }
            catch(Exception ex)
            {
                Console.WriteLine("Canot disconnect remote device {0}: {1}", serial, ex.Message);
            }

            this.Devices.Remove(serial);

            return dev_name;
        }

        private void OnDeviceBlackListed(string serial,string message, bool raise_device_exception)
        {
            if (this.blacklist.Contains(serial) == false)
            {
                Console.WriteLine("Adding device to blacklist: " + serial);
                this.blacklist.Add(serial);

                if (raise_device_exception)
                    this.DeviceException?.Invoke(new SgsException(message, null));

                this.Changed?.Invoke(this);
            }
        }

        private void HandleExceptions(Exception ex)
        {
            if (ex is ObjectDisposedException)
                return;

            if (ex is ThreadAbortException)
                return;

            string message = String.Format(Catalog.GetString("Unhandled exception while updating device list:{0}"), ex.Message);
            Console.WriteLine(message);
            throw new Exception(message, ex);
        }


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

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

        private void NotifyDeviceWarning(IDevice dev, string message)
        {
            this.DeviceWarning?.Invoke(dev, message);
        }

        private void DeviceIdleTaskNotify()
        {
            //TODO?
        }


        private UsbDeviceFinder DeviceFinder { get; set; }
        private bool IsListening { get; set; }
    }
}
