using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace NixUniversalSDK.Wrapper;

/// <summary>
/// Wrapper around a single <see cref="IDeviceScanner"/> instance
/// </summary>
public class DeviceScannerModule
{
    #region Static / constants
    /// <summary>
    /// Static instance of the <see cref="DeviceScannerModule"/>
    /// </summary>
    internal static readonly DeviceScannerModule Instance = new();

    /// <summary>
    /// Default / fallback value for the max reporting interval from the <see cref="ScanResultDelegate"/> callback.
    /// </summary>
    public const int DefaultMaxReportIntervalMs = 250;

    /// <summary>
    /// Default / fallback value for the max report length from the <see cref="ScanResultDelegate"/> callback.
    /// </summary>
    public const int DefaultMaxReportCount = 25;
    #endregion

    #region Delegates
    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceScannerEvents.ScannerCreated"/> event
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnScannerCreated(string senderName, int deviceScannerState)
    /// {
    ///     // - `senderName` is "ScannerCreated"
    ///     // - `deviceScannerState` is one of the 
    ///     //   `DeviceScannerState` values
    ///     // ...
    /// }
    /// </code>
    /// </example>
    public Delegates.IntValue? ScannerCreatedDelegate { get; internal set; }

    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceScannerEvents.ScannerStarted"/> event
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnScannerStarted(string senderName)
    /// {
    ///     // - `senderName` is "ScannerStarted"
    ///     // ...
    /// }
    /// </code>
    /// </example>
    public Delegates.Empty? ScannerStartedDelegate { get; internal set; }

    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceScannerEvents.ScannerStopped"/> event
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnScannerStopped(string senderName)
    /// {
    ///     // - `senderName` is "ScannerStopped"
    ///     // ...
    /// }
    /// </code>
    /// </example>
    public Delegates.Empty? ScannerStoppedDelegate { get; internal set; }

    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceScannerEvents.ScanResult"/> event
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnScanResult(string senderName, string resultsJson)
    /// {
    ///     // - `senderName` is "ScanResult"
    ///     // - `resultsJson` is a JSON array of scan results
    ///     // ...
    /// }
    /// </code>
    /// The scan results are JSON formatted in the same manner as <see cref="Exported.Scanner_GetSortedResults(int)"/>.
    /// </example>
    public Delegates.StringValue? ScanResultDelegate { get; internal set; }
    #endregion

    #region Constructors
    internal DeviceScannerModule()
    {               
        // Create DeviceScanner instance and set up its events
        Scanner = new DeviceScanner();
        Scanner.ScannerCreated += OnScannerCreated;
        Scanner.ScannerStarted += OnScannerStarted;
        Scanner.ScannerStopped += OnScannerStopped;
        Scanner.ScanResult += OnScanResult;

        // Initialize (async)
        ScannerInitTask = Scanner.InitializeAsync();
    }
    #endregion

    #region DeviceScanner instance
    internal IDeviceScanner Scanner { get; private set; }
    internal Task<DeviceScannerState> ScannerInitTask;
    internal readonly IDictionary<string, IDeviceCompat> Devices = 
        new ConcurrentDictionary<string, IDeviceCompat>();
    internal List<IDeviceCompat> SortedDevices => Devices.Values
        .Where(x => x is not null)
        .OrderBy(x => x.Rssi).Reverse()
        .ToList();
    
    internal ScannerResult[] GetSortedResults(int maxCount = -1)
    {
        // Fall back to list size if no argument was provided,
        // or if more devices requested than are available
        maxCount = ((maxCount < 0) || (maxCount > Devices.Count)) ? Devices.Count : maxCount;

        // Get sublist
        var subList = SortedDevices.GetRange(0, maxCount);

        // Form list of `ScannerResult` objects
        var results = new List<ScannerResult>();
        foreach (var device in subList)
        {
            if (device.AsScannerResult() is ScannerResult result)
            {
                results.Add(result);
            }                    
        }
        return [.. results];
    }

    internal bool TryGetDevice(string id, out IDeviceCompat? device)
    {
        var hasDevice = Devices.TryGetValue(id.ToUpperInvariant(), out device);
        if (hasDevice && device is not null) Devices.Remove(device.Id);
        return hasDevice;
    }

    private void OnScannerCreated(object? sender, ScannerCreatedEventArgs args) => 
        ScannerCreatedDelegate?.Invoke(
            senderName: nameof(IDeviceScannerEvents.ScannerCreated), 
            intValue: (int)args.State);

    private void OnScannerStarted(object? sender, EventArgs args) => 
        ScannerStartedDelegate?.Invoke(senderName: nameof(IDeviceScannerEvents.ScannerStarted));

    private void OnScannerStopped(object? sender, EventArgs args) => 
        ScannerStoppedDelegate?.Invoke(senderName: nameof(IDeviceScannerEvents.ScannerStopped));

    private void OnScanResult(
        object? sender,
        ScanResultEventArgs args)
    {
        if (args.Device is IDeviceCompat device)
        {
            // Add / update the device in the list
            Devices[device.Id] = device;

            // Callback with list, if necessary
            SendDeviceList(cooloffMs: MaxReportIntervalMs);
        }
    }

    private DateTime LastReportTime = DateTime.MinValue;
    private int MaxReportIntervalMs = DefaultMaxReportIntervalMs;
    private int MaxReportCount = DefaultMaxReportCount;

    /// <summary>
    /// Invokes the <see cref="ScanResultDelegate"/> callback at a limited interval
    /// </summary>
    /// <param name="cooloffMs">Interval in milliseconds used to limit the callback</param>
    private void SendDeviceList(int cooloffMs)
    {
        var timeSinceReport = DateTime.Now - LastReportTime;
        if (timeSinceReport.TotalMilliseconds > cooloffMs)
        {
            // Send report now
            ScanResultDelegate?.Invoke(
                senderName: nameof(IDeviceScannerEvents.ScanResult),
                stringValue: GetSortedResults(MaxReportCount).AsJsonString());

            // Update LastReportTime
            LastReportTime = DateTime.Now;
        }
    }

    /// <summary>
    /// Resets the <see cref="DeviceScanner"/>
    /// </summary>
    internal async void ResetAsync()
    {
        // Check if init task is completed
        // If not completed, then the scanner is already resetting
        if (ScannerInitTask.IsCompleted)
        {
            // Scanner init task has completed
            // Stop the scanner and re-initialize
            if (Scanner is IDeviceScanner scanner)
            {
                scanner.Stop();
                ScannerInitTask = scanner.InitializeAsync();
            }
        }

        // Wait for init to complete
        await ScannerInitTask;
    }

    /// <summary>
    /// Updates the devices list with attached USB devices only
    /// </summary>
    internal async Task UpdateUsbDeviceListAsync()
    {
        // Stop the scanner if it is already running
        if (Scanner?.State == DeviceScannerState.Scanning)
        {
            Scanner?.Stop();
        }            

        // Clear the list
        Devices.Clear();

        // Run USB listing
        IEnumerable<IDeviceCompat>? usbDeviceList = null;
        if (Scanner is IDeviceScanner scanner)
        {
            usbDeviceList = await scanner.ListUsbDevicesAsync();
        }
        if (usbDeviceList is not null && usbDeviceList.Any())
        {
            // Add/update all seen USB devices in the devices list
            foreach (var usbDevice in usbDeviceList)
            {
                if (usbDevice is IDeviceCompat nonNullDevice)
                {
                    Devices[nonNullDevice.Id] = nonNullDevice;
                }
            }
        }
    }

    /// <summary>
    /// Starts a device search for the specified time and awaits its completion
    /// </summary>
    /// <param name="scanPeriodMs">Duration for the device search</param>
    /// <param name="maxReportIntervalMs">Maximum interval in milliseconds to invoke the <see cref="ScanResultDelegate"/> callback</param>
    /// <param name="maxReportCount">Maximum number of devices to report in the <see cref="ScanResultDelegate"/> callback</param>
    /// <returns>Task which completes when the device search ends</returns>
    internal async Task StartDeviceSearchAsync(
        int scanPeriodMs = (int)DeviceScanner.DefaultGeneralScanPeriodMs,
        int maxReportIntervalMs = DefaultMaxReportIntervalMs,
        int maxReportCount = DefaultMaxReportCount)
    {
        // Stop the scanner if it is already running
        if (Scanner?.State == DeviceScannerState.Scanning)
        {
            Scanner?.Stop();
        }

        // Clear the list
        Devices.Clear();

        // Reset last report timestamp and set other reporting parameters
        LastReportTime = DateTime.MinValue;
        MaxReportIntervalMs = maxReportIntervalMs;
        MaxReportCount = maxReportCount;

        // Check DeviceScanner state
        if (Scanner?.State != DeviceScannerState.Idle)
        {
            // Cannot start the scanner because its state is invalid
            // List USB devices anyways
            await UpdateUsbDeviceListAsync();

            // Send callback with device list
            SendDeviceList(cooloffMs: 0);
        }
        else
        {
            // Start the search and watch for the scanner to stop
            var completionSource = new TaskCompletionSource<bool>(
                TaskCreationOptions.RunContinuationsAsynchronously);
            Scanner.ScannerStopped += Local_OnScannerStopped;
            Scanner.Start(scanPeriodMs);
            await completionSource.Task;

            void Local_OnScannerStopped(object? sender, EventArgs args)
            {
                // Scanner has stopped
                // Remove this event
                if (Scanner is not null) { Scanner.ScannerStopped -= Local_OnScannerStopped; }

                // Send the scan report / list one last time if it is not empty.
                // Do this regardless of when the last report was sent, since the
                // list may have changed
                if (Devices.Count > 0)
                {
                    SendDeviceList(cooloffMs: 0);
                }

                // Complete the task
                completionSource.TrySetResult(true);
            }
        }
    }

    /// <summary>
    /// Searches for a specific Nix device via USB and Bluetooth. The search will run until the specified device is found, or until the specified time interval elapses, whichever is shorter.
    /// </summary>
    /// <param name="id">Specific device ID to find</param>
    /// <param name="scanPeriodMs">Maximum time period in milliseconds to run the search</param>
    /// <returns>Task which completes when the device search ends</returns>
    internal async Task SearchForId(
        string id,
        int scanPeriodMs = (int)DeviceScanner.DefaultSingleDevicePeriodMs)
    {
        try
        {
            // Remove event handler for ScanResult
            if (Scanner is not null) { Scanner.ScanResult -= OnScanResult; }

            // Stop the scanner if it is already running
            if (Scanner?.State == DeviceScannerState.Scanning)
            {
                Scanner?.Stop();
            }

            // Clear the list
            Devices.Clear();

            // Reset last report timestamp and set other reporting parameters
            LastReportTime = DateTime.MinValue;
            MaxReportIntervalMs = 0;
            MaxReportCount = 1;

            // Check DeviceScanner state
            if (Scanner?.State != DeviceScannerState.Idle)
            {
                // Cannot start the scanner because its state is invalid
                // ...
            }
            else
            {
                if ((await Scanner.SearchForIdAsync(id, scanPeriodMs)) is IDeviceCompat device)
                {
                    // Device was found, add it to the list
                    Devices[device.Id] = device;
                }
            }
        }
        catch { }
        finally
        {
            // Re-attach event handler for ScanResult
            if (Scanner is not null) { Scanner.ScanResult += OnScanResult; }
        }
    }
    #endregion
}