using System;
using System.Threading.Tasks;

namespace NixUniversalSDK.Wrapper;

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

    #region Delegates
    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceCompatEvents.Connected"/> event
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnConnected(string senderName)
    /// {
    ///     // - `senderName` is "Connected"
    ///     // ...
    /// }
    /// </code>
    /// </example>
    public Delegates.Empty? ConnectedDelegate { get; internal set; }

    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceCompatEvents.Disconnected"/> event. The provided integer value corresponds to the <see cref="DeviceStatus"/> argument from the event.
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnDisconnected(string senderName, int deviceStatus)
    /// {
    ///     // - `senderName` is "Disconnected"
    ///     // - `deviceStatus` is one of the `DeviceStatus` values
    ///     
    ///     // Handle status codes here, if desired in your application
    ///     // At minimum, should check for `ErrorUnauthorized` (5) status
    ///     switch (deviceScannerStatus)
    ///     {
    ///         case 5:
    ///             // `DeviceStatus.ErrorUnauthorized`
    ///             // Device not authorized for this SDK build
    ///             // ..
    ///             break;
    ///         case 0:
    ///             // `DeviceStatus.Success`
    ///             // Normal disconnect from `Device_Disconnect()`
    ///             // ...
    ///             break;
    ///         case 4:
    ///             // `DeviceStatus.ErrorDroppedConnection`
    ///             // Nix device dropped the connection
    ///             // ...
    ///             break;
    ///         case 1:
    ///             // `DeviceStatus.ErrorMaxAttempts`
    ///         case 2:
    ///             // `DeviceStatus.ErrorTimeout`
    ///             // Connection to Nix device timed out
    ///             // ...
    ///             break;
    ///         default:
    ///             // Other internal errors
    ///             // ..
    ///             break;
    ///     }
    /// }
    /// </code>
    /// </example>
    public Delegates.IntValue? DisconnectedDelegate { get; internal set; }

    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceCompatEvents.BatteryStateChanged"/> event. The provided integer value corresponds to the new battery state.
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnBatteryStateChanged(string senderName, int newState)
    /// {
    ///     // - `senderName` is "BatteryStateChanged"
    ///     // - `newState` is the updated battery level (0 - 100)
    ///     // ...
    /// }
    /// </code>
    /// </example>
    public Delegates.IntValue? BatteryStateChangedDelegate { get; internal set; }

    /// <summary>
    /// Delegate linked to the internal <see cref="IDeviceCompatEvents.ExtPowerStateChanged"/> event. The provided boolean value corresponds to the new external power state.
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnBatteryStateChanged(string senderName, bool newState)
    /// {
    ///     // - `senderName` is "ExtPowerStateChanged"
    ///     // - `newState` is the updated external power state: 
    ///     //   `true` when power is connected, `false` when disconnected
    ///     // ...
    /// }
    /// </code>
    /// </example>
    public Delegates.BoolValue? ExtPowerStageChangedDelegate { get; internal set; }

    /// <summary>
    /// Delegate that is invoked on completion of any async task by the connected device. The `senderId` string value corresponds to the name of the async task that was completed, and the provided integer value corresponds to the <see cref="CommandStatus"/> value of the result.
    /// </summary>
    /// <example>
    /// An example handler is shown below:
    /// <code>
    /// void OnCommandCompletedDelegate(string senderName, int commandStatus)
    /// {
    ///     // - `senderName` is the async task / function name from 
    ///     //   `IDeviceCompat` that invoked this handler.
    ///     // - `commandStatus` is one of the `CommandStatus` values
    ///     
    ///     if (senderName.Equals(
    ///         "MeasureAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `MeasureAsync`
    ///         // ...
    ///     }
    ///     else if (senderName.Equals(
    ///         "RunFieldCalibrationAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `RunFieldCalibrationAsync`
    ///         // ...
    ///     }
    ///     else if (senderName.Equals(
    ///         "SetFieldCalibrationEnabledAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `SetFieldCalibrationEnabledAsync`
    ///         // ...
    ///     }
    ///     else if (senderName.Equals(
    ///         "SetTemperatureCompensationEnabledAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `SetTemperatureCompensationEnabledAsync`
    ///         // ...
    ///     }
    ///     else if (senderName.Equals(
    ///         "SetHapticFeedbackEnabledAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `SetHapticFeedbackEnabledAsync`
    ///         // ...
    ///     }
    ///     else if (senderName.Equals(
    ///         "SetRgbFeedbackEnabledAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `SetRgbFeedbackEnabledAsync`
    ///         // ...
    ///     }
    ///     else if (senderName.Equals(
    ///         "LedTestAsync", 
    ///         StringComparison.OrdinalIgnoreCase))
    ///     {
    ///         // Callback from `LedTestAsync`
    ///         // ...
    ///     }
    /// }
    /// </code>
    /// </example>
    public Delegates.IntValue? CommandCompletedDelegate { get; internal set; }
    #endregion

    #region Constructors
    internal DeviceCompatModule() 
    {
    }
    #endregion

    #region IDeviceCompat instance
    private IDeviceCompat? _Device = null;
    internal IDeviceCompat? Device
    {
        get => _Device;
        private set
        {
            // Disconnect from old device if not null
            if (_Device is IDeviceCompat oldDevice)
            {
                // Remove device event handlers first to prevent extraneous callbacks
                RemoveDeviceEvents(oldDevice);
                oldDevice.Disconnect();
            }

            // Update device reference
            _Device = value;

            // Connect to new device if not null
            if (value is IDeviceCompat newDevice)
            {
                // Add device event handlers
                AddDeviceEvents(newDevice);

                // Start connection process
                newDevice.ConnectAsync();
            }
        }
    }

    private void AddDeviceEvents(IDeviceCompat device)
    {
        if (device is not null)
        {
            // Un-register
            device.Connected -= OnConnected;
            device.Disconnected -= OnDisconnected;
            device.BatteryStateChanged -= OnBatteryStateChanged;
            device.ExtPowerStateChanged -= OnExtPowerStateChanged;

            // Re-register                
            device.Connected += OnConnected;
            device.Disconnected += OnDisconnected;
            device.BatteryStateChanged += OnBatteryStateChanged;
            device.ExtPowerStateChanged += OnExtPowerStateChanged;
        }
    }

    private void RemoveDeviceEvents(IDeviceCompat device)
    {
        if (device is not null)
        {
            device.Connected -= OnConnected;
            device.Disconnected -= OnDisconnected;
            device.BatteryStateChanged -= OnBatteryStateChanged;
            device.ExtPowerStateChanged -= OnExtPowerStateChanged;
        }
    }

    private void OnConnected(
        object? sender,
        EventArgs args) => ConnectedDelegate?.Invoke(
            senderName: nameof(IDeviceCompatEvents.Connected));

    private void OnDisconnected(
        object? sender,
        DeviceStatusArgs args)
    {            
        if (sender is IDeviceCompat device)
        {
            // Remove device event handlers
            RemoveDeviceEvents(device);
        }

        // Invoke callback
        DisconnectedDelegate?.Invoke(
            senderName: nameof(IDeviceCompatEvents.Disconnected),
            intValue: (int)args.Status);
    }

    private void OnBatteryStateChanged(
        object? sender,
        BatteryStateEventArgs args) => BatteryStateChangedDelegate?.Invoke(
            senderName: nameof(IDeviceCompatEvents.BatteryStateChanged),
            intValue: args.NewState);

    private void OnExtPowerStateChanged(
        object? sender,
        ExtPowerStateEventArgs args) => ExtPowerStageChangedDelegate?.Invoke(
            senderName: nameof(IDeviceCompatEvents.ExtPowerStateChanged),
            boolValue: args.NewState);
    #endregion

    #region IDeviceCompat instance helpers
    internal async Task Connect(string id)
    {
        // If connected to a device, disconnect now
        if (Device is IDeviceCompat oldDevice)
        {
            // Remove device events first to suppress extra 'Disconnected' event
            RemoveDeviceEvents(oldDevice);
            oldDevice.Disconnect();
        }

        // Get instance of the DeviceScannerModule
        var scannerModule = DeviceScannerModule.Instance;

        // Wait for the DeviceScanner to initialize, if necessary
        await scannerModule.ScannerInitTask;

        // Get the specified device, searching if necessary
        if (!scannerModule.Devices.ContainsKey(id))
        {
            await scannerModule.SearchForId(
                id: id, 
                scanPeriodMs: (int)DeviceScanner.DefaultGeneralScanPeriodMs);
        }
        if (scannerModule.TryGetDevice(id, out var device))
        {
            // Stop the Scanner before starting the connection routine
            scannerModule.Scanner?.Stop();

            // Save a reference to this device
            // Connection is triggered in the setter for `Device`
            Device = device;

            // No callback necessary from this point...
            // Subsequent callbacks will be invoked by the device events
            // ...
        }
        else
        {
            // Device has not been found by the DeviceScanner
            // Callback with error
            DisconnectedDelegate?.Invoke(
                senderName: nameof(IDeviceCompatEvents.Disconnected),
                intValue: (int)DeviceStatus.ErrorTimeout);
        }
    }

    internal async void SetFieldCalibrationEnabled(bool value)
    {
        // Send the `SetFieldCalibrationEnabledAsync` command and await the result            
        DeviceResult? result = null;
        if (Device is IDeviceCompat device)
        {
            result = await device.SetFieldCalibrationEnabledAsync(value);
        }

        // Callback
        var status = result?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.SetFieldCalibrationEnabledAsync),
            intValue: (int)status);
    }

    internal async void SetTemperatureCompensationEnabled(bool value)
    {
        // Send the `SetTemperatureCompensationEnabledAsync` command and await the result
        DeviceResult? result = null;
        if (Device is IDeviceCompat device)
        {
            result = await device.SetTemperatureCompensationEnabledAsync(value);
        }

        // Callback
        var status = result?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.SetTemperatureCompensationEnabledAsync),
            intValue: (int)status);
    }

    internal async void SetHapticFeedbackEnabled(bool value)
    {
        // Send the `SetHapticFeedbackEnabledAsync` command and await the result
        DeviceResult? result = null;
        if (Device is IDeviceCompat device)
        {
            result = await device.SetHapticFeedbackEnabledAsync(value);
        }

        // Callback
        var status = result?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.SetHapticFeedbackEnabledAsync),
            intValue: (int)status);
    }

    internal async void SetRgbFeedbackEnabled(bool value)
    {
        // Send the `SetRgbFeedbackEnabledAsync` command and await the result
        DeviceResult? result = null;
        if (Device is IDeviceCompat device)
        {
            result = await device.SetRgbFeedbackEnabledAsync(value);
        }

        // Callback
        var status = result?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.SetRgbFeedbackEnabledAsync),
            intValue: (int)status);
    }

    internal async void LedTest()
    {
        // Send the `LedTestAsync` command to the device and await the result
        DeviceResult? result = null;
        if (Device is IDeviceCompat device)
        {
            result = await device.LedTestAsync();
        }

        // Callback
        var status = result?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.LedTestAsync),
            intValue: (int)status);
    }

    internal async void Measure(params ScanMode[] modes)
    {
        // Clear the last measurement result
        LastMeasurementResult = null;
        
        // Send the `MeasureAsync` command to device
        // Await and store result
        if (Device is IDeviceCompat device)
        {
            LastMeasurementResult = await device.MeasureAsync(modes);
        }

        // Callback
        var status = LastMeasurementResult?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.MeasureAsync),
            intValue: (int)status);
    }

    internal async void RunFieldCalibration(string tileString)
    {
        // Clear the last field calibration result
        LastCalibrationResult = null;

        // Send the `RunFieldCalibrationAsync` command to the device
        // Await and store the result
        if (Device is IDeviceCompat device)
        {
            LastCalibrationResult = await device.RunFieldCalibrationAsync(tileString);
        }

        // Callback
        var status = LastCalibrationResult?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.RunFieldCalibrationAsync),
            intValue: (int)status);
    }
    internal async void InvalidateFieldCalibration()
    {
        // Send the `InvalidateFieldCalibrationAsync` command to the device
        DeviceResult? result = null;
        if (Device is IDeviceCompat device)
        {
            result = await device.InvalidateFieldCalibrationAsync();
        }

        // Callback
        var status = result?.Status ?? CommandStatus.ErrorInternal;
        CommandCompletedDelegate?.Invoke(
            senderName: nameof(IDeviceCompat.InvalidateFieldCalibrationAsync),
            intValue: (int)status);
    }
    #endregion

    #region Last measurement record
    internal DateTime LastMeasurementTime = DateTime.MinValue;
    private DeviceResult? _LastMeasurementResult = null;
    internal DeviceResult? LastMeasurementResult
    {
        get => _LastMeasurementResult;
        set
        {
            _LastMeasurementResult = value;
            LastMeasurementTime = DateTime.UtcNow;
        }
    }
    internal DeviceResult? LastCalibrationResult = null;        
    internal bool TryGetMeasurement(string scanModeName, out IMeasurementData? measurement)
    {
        // Try to parse scan mode name to enum
        var isNameValid = Enum.TryParse<ScanMode>(
            value: scanModeName,
            ignoreCase: true,
            result: out var scanMode);

        // Try to get measurement value from dictionary
        measurement = null;
        var hasMeasurement = LastMeasurementResult?
            .Measurements?
            .TryGetValue(scanMode, out measurement) ?? false;

        return isNameValid && hasMeasurement;
    }
    #endregion
}