using System.Collections.Concurrent;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using NixExampleWinUI.Models;
using NixExampleWinUI.Views;
using NixUniversalSDK;
using Windows.ApplicationModel.Resources;

namespace NixExampleWinUI.ViewModels;

/// <summary>
/// View model for <see cref="MainPage"/>
/// </summary>
public class MainViewModel : ObservableRecipient
{
    #region Static / constants
    private const string LogPrefix = nameof(MainViewModel) + "/";

    /// <summary>
    /// 'Refresh' glyph, see https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font
    /// </summary>
    private const string GlyphRefresh = "\ue72c";

    /// <summary>
    /// 'Cancel' glyph, see https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font
    /// </summary>
    private const string GlyphCancel = "\ue711";

    /// <summary>
    /// Resource loader used to fetch string resources
    /// </summary>
    private static readonly ResourceLoader Loader = ResourceLoader.GetForViewIndependentUse();

    /// <summary>
    /// DeviceModel singleton, used to hold instances of the `DeviceScanner` and connected `IDeviceCompat` instances
    /// </summary>
    private readonly DeviceModel DeviceModel;

    /// <summary>
    /// Set to a number greater than 0 to limit how frequently the device list
    /// updates (eg - 250 ms), or set to 0 to refresh the list on screen for 
    /// every scan result.
    /// </summary>
    private const uint ListUpdateLimitMs = 0;
    #endregion

    #region Constructors
    public MainViewModel()
    {
        // Get `DeviceModel` singleton
        DeviceModel = App.GetService<DeviceModel>();
    }
    #endregion

    #region DeviceScanner
    public IDeviceScanner Scanner => DeviceModel.Scanner;
    public DeviceScannerState ScannerState => Scanner.State;
    public bool IsSearching => ScannerState == DeviceScannerState.Scanning;
    private bool IsScannerResettable => !IsSearching;

    public async Task<DeviceScannerState> UpdateDeviceScannerStateAsync()
    {
        if (Scanner.State == DeviceScannerState.Cold)
        {
            // Scanner is still initializing
            await DeviceModel.ScannerInitTask;
        }
        
        if (IsScannerResettable)
        {
            _ = await Scanner.InitializeAsync();
            AddDeviceScannerEvents();
        }
        return Scanner.State;
    }

    public async Task UpdateUsbDeviceListAsync()
    {
        var usbDeviceList = await Scanner.ListUsbDevicesAsync();
        if (usbDeviceList != 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;
                }
            }
            OnPropertyChanged(nameof(SortedDevices));
        }
    }
    #endregion

    #region DeviceScanner events
    private void AddDeviceScannerEvents()
    {
        if (Scanner is not null)
        {
            Scanner.ScannerStarted += OnScannerStarted;
            Scanner.ScannerStopped += OnScannerStopped;
            Scanner.ScanResult += OnScanResult;
        }        
    }
    private void OnScannerStarted(object? sender, EventArgs args)
    {
        LastListUpdateTicks = 0;
        UpdateDisplayedScannerProperties();
    }
    private void OnScannerStopped(object? sender, EventArgs args)
    {        
        UpdateDisplayedScannerProperties();
        UpdateDevicesList(cooloffMs: 0);
    }
    private void OnScanResult(object? sender, ScanResultEventArgs args)
    {
        if (sender != Scanner) { return; }

        // Update device instance in the list
        if (args.Device is IDeviceCompat device)
        {
            Devices[device.Id] = device;
        }

        // Update on-screen list
        UpdateDevicesList();
    }

    private long LastListUpdateTicks = 0;
    private void UpdateDevicesList(long cooloffMs = ListUpdateLimitMs)
    {
        var now = DateTime.Now.Ticks;
        if ((now - LastListUpdateTicks) > (cooloffMs * TimeSpan.TicksPerMillisecond))
        {
            LastListUpdateTicks = now;
            OnPropertyChanged(nameof(SortedDevices));
        }
    }
    #endregion

    #region List of enumerated Nix devices
    private readonly ConcurrentDictionary<string, IDeviceCompat> Devices = new();
    public IEnumerable<IDeviceCompat> SortedDevices => Devices.Values
        .Where(x => x is not null)
        .OrderBy(x => x.Rssi).Reverse();
    public void ClearDevices()
    {
        Devices.Clear();
        UpdateDevicesList(cooloffMs: 0);
    }
    #endregion

    #region IDeviceCompat and events
    public IDeviceCompat? SelectedDevice
    {
        get => DeviceModel.Device;
        set 
        {
            // If connected to a device already, disconnect now
            RemoveDeviceEvents();
            DeviceModel.Device?.Disconnect();

            // Update stored device value
            DeviceModel.Device = value;

            if (DeviceModel.Device is not null)
            {
                // Add events
                AddDeviceEvents();

                // Start connection... once completed, the `Connected` or `Disconnected` event will be invoked
                DeviceModel.Device.ConnectAsync();
            }

            // Update UI
            UpdateDisplayedDeviceProperties();
        }
    }

    private void AddDeviceEvents()
    {
        if (SelectedDevice is not null)
        {
            SelectedDevice.Connected += OnConnected;
            SelectedDevice.Disconnected += OnDisconnected;
        }    
    }

    private void RemoveDeviceEvents()
    {
        if (SelectedDevice is not null)
        {
            SelectedDevice.Connected -= OnConnected;
            SelectedDevice.Disconnected -= OnDisconnected;
        }
    }

    private void OnConnected(object? sender, EventArgs args)
    {
        // Show status on screen
        if (DeviceInfoBar is not null)
        {
            DeviceInfoBar.Title = string.Empty;
            DeviceInfoBar.Message = string.Format(Loader.GetString("MessageConnected"), SelectedDevice?.Name, SelectedDevice?.Id); ;
            DeviceInfoBar.Severity = InfoBarSeverity.Success;
            DeviceInfoBar.IsOpen = true;
        }
        UpdateDisplayedDeviceProperties();
    }

    private void OnDisconnected(object? sender, DeviceStatusArgs args)
    {
        // Show status on screen
        if (DeviceInfoBar is not null)
        {
            DeviceInfoBar.Title = Loader.GetString("TitleDisconnected");
            DeviceInfoBar.Message = string.Format(Loader.GetString("MessageDisconnected"), args.Status.GetName());
            DeviceInfoBar.Severity = args.Status == DeviceStatus.Success ? InfoBarSeverity.Success : InfoBarSeverity.Error;
            DeviceInfoBar.IsOpen = true;
        }

        // Clear selected device
        SelectedDevice = null;
        UpdateDisplayedDeviceProperties();
    }
    #endregion

    #region UI bindings
    public InfoBar? DeviceInfoBar { get; set; }
    public ColorSwatch? MeasuredSwatch { get; set; }
    public SpectralPlot? SpectralPlot { get; set; }
    public string ScannerRefreshLabel => 
        (IsSearching ? Loader.GetString("ActionStop") : 
        Loader.GetString("ActionRefresh")) ?? string.Empty;
    public string ScannerRefreshGlyph => IsSearching ? GlyphCancel : GlyphRefresh;
    public void UpdateDisplayedScannerProperties()
    {
        OnPropertyChanged(nameof(ScannerRefreshLabel));
        OnPropertyChanged(nameof(ScannerRefreshGlyph));
        OnPropertyChanged(nameof(IsSearching));
    }
    public bool IsDeviceSelected => SelectedDevice is not null;
    public bool IsDeviceConnecting => SelectedDevice?.State == DeviceState.BusyConnecting;
    public bool IsDeviceBusy => SelectedDevice?.State switch
    {
        DeviceState.Disconnected or DeviceState.Idle or null => false,
        _ => true
    };
    public void UpdateDisplayedDeviceProperties()
    {
        OnPropertyChanged(nameof(IsDeviceSelected));
        OnPropertyChanged(nameof(IsDeviceConnecting));
        OnPropertyChanged(nameof(IsDeviceBusy));        
    }
    #endregion

    #region Measurement data
    private IDictionary<ScanMode, IMeasurementData> _Measurements = new Dictionary<ScanMode, IMeasurementData>();
    public IDictionary<ScanMode, IMeasurementData> Measurements
    {
        get => _Measurements;
        set
        {
            // Update stored value
            _Measurements = value;

            if (value.Count > 0)
            {
                // Select a scan mode, this will trigger a re-draw of the plot and color
                // Keep the previously selected mode, falling back to the first entry if the past selection is no longer available
                var newMode = IsModeAvailable(SelectedMode) ? SelectedMode : Measurements.Keys.FirstOrDefault();
                SelectedMode = newMode;
            }
        }
    }
    private bool IsModeAvailable(ScanMode mode) => Measurements.ContainsKey(mode);
    public bool IsM0Available => IsModeAvailable(ScanMode.M0);
    public bool IsM1Available => IsModeAvailable(ScanMode.M1);
    public bool IsM2Available => IsModeAvailable(ScanMode.M2);
    private ScanMode _SelectedMode = ScanMode.NA;
    public ScanMode SelectedMode
    {
        get => _SelectedMode;
        set
        {
            // Ignore 'NA' values
            if (value == ScanMode.NA) { return; }
            
            // Update stored value
            _SelectedMode = value;

            // Set value to plot container
            if (SpectralPlot is not null) {
                SpectralPlot.MeasurementData = SelectedMeasurement;
            }

            // Set value to color swatch
            if (MeasuredSwatch is not null)
            {
                MeasuredSwatch.MeasurementData = SelectedMeasurement;
            }

            // Update UI
            var propertyNames = new List<string?>
            {
                nameof(SelectedMode),
                nameof(IsM0Available),
                nameof(IsM1Available),
                nameof(IsM2Available)
            };
            foreach (var name in propertyNames) { OnPropertyChanged(name); };
        }
    }
    public IMeasurementData? SelectedMeasurement =>
        Measurements.TryGetValue(SelectedMode, out var value) ? value : null;
    #endregion
}