using System.Threading;
using Eto.Drawing;
using Eto.Forms;
using NAPS2.EtoForms.Layout;
using NAPS2.EtoForms.Widgets;
using NAPS2.Lang;
using NAPS2.Scan;
using NAPS2.Scan.Internal;

namespace NAPS2.EtoForms.Ui;

public class ChooseDeviceForm : EtoDialogBase
{
    private const string FLATPAK_MISSING_SCANNER_URL = "https://www.naps2.com/doc/flatpak-missing-scanner";

    private readonly ScanDevice AlwaysAskMarker = new(Driver.Default, "*always*ask*", UiStrings.AlwaysAsk);
    private readonly ScanDevice ManualIpMarker = new(Driver.Default, "*manual*ip*", UiStrings.ManualIp);

    private readonly RadioButton _wiaDriver;
    private readonly RadioButton _twainDriver;
    private readonly RadioButton _appleDriver;
    private readonly RadioButton _saneDriver;
    private readonly RadioButton _esclDriver;
    private readonly IIconProvider _iconProvider;
    private readonly DeviceListViewBehavior _deviceListViewBehavior;
    private readonly ScanningContext _scanningContext;
    private readonly DeviceCapsCache _deviceCapsCache;
    private readonly LayoutVisibility _textListVis = new(false);
    private readonly ListBox _deviceTextList = new();
    private readonly IListView<ScanDevice> _deviceIconList;
    private readonly Button _selectDevice;
    // TODO: The spinner doesn't seem to animate on WinForms
    private readonly Spinner _spinner = new() { Enabled = true };
    private readonly ImageView _statusIcon = new();
    private readonly Label _statusLabel = new() { Text = UiStrings.SearchingForDevices };

    private readonly LinkButton _flatpakMissingDevice =
        C.UrlLink(FLATPAK_MISSING_SCANNER_URL, UiStrings.CantFindScannerFlatpak);

    private readonly LayoutVisibility _saneDriverVis = new(false);
    private readonly LayoutVisibility _spinnerVis = new(true);

    private CancellationTokenSource? _getDevicesCts;
    private Driver? _activeQuery;
    private string? _statusIconName;

    public ChooseDeviceForm(Naps2Config config, IIconProvider iconProvider,
        DeviceListViewBehavior deviceListViewBehavior, ScanningContext scanningContext,
        DeviceCapsCache deviceCapsCache) : base(config)
    {
        _iconProvider = iconProvider;
        _deviceListViewBehavior = deviceListViewBehavior;
        _scanningContext = scanningContext;
        _deviceCapsCache = deviceCapsCache;
        _selectDevice = C.OkButton(this, SelectDevice, UiStrings.Select);
        _deviceIconList = EtoPlatform.Current.CreateListView(deviceListViewBehavior);
        _deviceIconList.ImageSize = new Size(48, 32);
        deviceListViewBehavior.SetIconName(AlwaysAskMarker, "ask");
        deviceListViewBehavior.SetIconName(ManualIpMarker, "network_ip");

        EtoPlatform.Current.AttachDpiDependency(this, _ =>
        {
            _deviceIconList.RegenerateImages();
            UpdateStatusIcon();
        });

        _deviceTextList.Activated += (_, _) => _selectDevice.PerformClick();
        _deviceIconList.ItemClicked += (_, _) => _selectDevice.PerformClick();

        _wiaDriver = new RadioButton { Text = UiStrings.WiaDriver };
        _twainDriver = new RadioButton(_wiaDriver) { Text = UiStrings.TwainDriver };
        _appleDriver = new RadioButton(_wiaDriver) { Text = UiStrings.AppleDriver };
        _saneDriver = new RadioButton(_wiaDriver) { Text = UiStrings.SaneDriver };
        _esclDriver = new RadioButton(_wiaDriver) { Text = UiStrings.EsclDriver };

        _textListVis.IsVisible = config.Get(c => c.DeviceListAsTextOnly);
    }

    private void UpdateStatusIcon()
    {
        if (_statusIconName != null)
        {
            _statusIcon.Image = _iconProvider.GetIcon(_statusIconName, EtoPlatform.Current.GetScaleFactor(this));
            _statusIcon.Size = Size.Round(new SizeF(16, 16) * EtoPlatform.Current.GetLayoutScaleFactor(this));
        }
    }

    private void Driver_MouseUp(object? sender, EventArgs e)
    {
        QueryForDevices();
    }

    private void Driver_CheckedChanged(object? sender, EventArgs e)
    {
        QueryForDevices();
    }

    private Driver DeviceDriver
    {
        get => _twainDriver.Checked ? Driver.Twain
            : _wiaDriver.Checked ? Driver.Wia
            : _appleDriver.Checked ? Driver.Apple
            : _saneDriver.Checked ? Driver.Sane
            : _esclDriver.Checked ? Driver.Escl
            : ScanOptionsValidator.SystemDefaultDriver;
        set
        {
            if (value == Driver.Twain)
            {
                _twainDriver.Checked = true;
            }
            else if (value == Driver.Wia)
            {
                _wiaDriver.Checked = true;
            }
            else if (value == Driver.Apple)
            {
                _appleDriver.Checked = true;
            }
            else if (value == Driver.Sane)
            {
                _saneDriver.Checked = true;
            }
            else if (value == Driver.Escl)
            {
                _esclDriver.Checked = true;
            }
        }
    }

    protected override void BuildLayout()
    {
        // TODO: Don't show if only one driver is available
        var driverElements = new List<LayoutElement>();
        if (PlatformCompat.System.IsWiaDriverSupported)
        {
            driverElements.Add(_wiaDriver.Scale());
        }
        if (PlatformCompat.System.IsTwainDriverSupported)
        {
            driverElements.Add(_twainDriver.Scale());
        }
        if (PlatformCompat.System.IsAppleDriverSupported)
        {
            driverElements.Add(_appleDriver.Scale());
        }
        if (PlatformCompat.System.IsSaneDriverSupported)
        {
            driverElements.Add(_saneDriver.Scale());
        }
        if (PlatformCompat.System.IsEsclDriverSupported)
        {
            driverElements.Add(_esclDriver.Scale());
        }

        Title = TranslationMigrator.PickTranslated(UiStrings.ResourceManager, nameof(UiStrings.SelectSource),
            nameof(UiStrings.SelectDevice));

        FormStateController.SaveFormState = FormStateController.RestoreFormState = true;
        FormStateController.DefaultExtraLayoutSize = new Size(150, 100);

        LayoutController.Content = L.Column(
            L.Row(
                [
                    ..driverElements,
                    C.IconButton("large_tiles_small", () => SetListView(false))
                        .Visible(_textListVis),
                    C.IconButton("text_align_justify_small", () => SetListView(true))
                        .Visible(!_textListVis)
                ]
            ),
            _deviceIconList.Control.NaturalSize(150, 100).Scale().Visible(!_textListVis),
            _deviceTextList.NaturalSize(150, 100).Scale().Visible(_textListVis),
#if NET6_0_OR_GREATER
            OperatingSystem.IsLinux() && File.Exists("/.flatpak-info")
                ? _flatpakMissingDevice.Visible(_saneDriverVis)
                : C.None(),
#endif
            L.Row(
                _spinner.Visible(_spinnerVis).AlignCenter(),
                _statusIcon.Visible(!_spinnerVis).AlignCenter(),
                _statusLabel.AlignCenter(),
                C.Filler(),
                L.OkCancel(_selectDevice, C.CancelButton(this))
            )
        );
    }

    private void SetListView(bool value)
    {
        _textListVis.IsVisible = value;
        Config.User.Set(c => c.DeviceListAsTextOnly, value);
    }

    protected override void OnShown(EventArgs e)
    {
        base.OnShown(e);

        DeviceDriver = ScanOptions!.Driver;

        _wiaDriver.CheckedChanged += Driver_CheckedChanged;
        _twainDriver.CheckedChanged += Driver_CheckedChanged;
        _appleDriver.CheckedChanged += Driver_CheckedChanged;
        _saneDriver.CheckedChanged += Driver_CheckedChanged;
        _esclDriver.CheckedChanged += Driver_CheckedChanged;
        // TODO: Maybe have a refresh button instead? As part of the status indicator?
        _wiaDriver.MouseUp += Driver_MouseUp;
        _twainDriver.MouseUp += Driver_MouseUp;
        _appleDriver.MouseUp += Driver_MouseUp;
        _saneDriver.MouseUp += Driver_MouseUp;
        _esclDriver.MouseUp += Driver_MouseUp;

        QueryForDevices();
    }

    private async void QueryForDevices()
    {
        if (_activeQuery == DeviceDriver)
        {
            return;
        }

        _saneDriverVis.IsVisible = _saneDriver.Checked;
        _deviceIconList.ImageSize = DeviceDriver == Driver.Escl ? new Size(48, 48) : new Size(48, 32);

        _getDevicesCts?.Cancel();
        _spinnerVis.IsVisible = true;
        _statusLabel.Text = UiStrings.SearchingForDevices;
        _activeQuery = DeviceDriver;

        DeviceList = new List<ScanDevice>();
        DeviceSet = new HashSet<ScanDevice>();
        ExtraItems = new List<ScanDevice>();
        if (DeviceDriver == Driver.Escl)
        {
            ExtraItems.Add(ManualIpMarker);
        }
        if (AllowAlwaysAsk && DeviceDriver is not (Driver.Wia or Driver.Twain))
        {
            ExtraItems.Add(AlwaysAskMarker);
        }

        UpdateDevices(true);

        var cts = new CancellationTokenSource();
        _getDevicesCts = cts;
        var optionsWithDriver = ScanOptions!.Clone();
        optionsWithDriver.Driver = DeviceDriver;
        var controller = new ScanController(_scanningContext);

        try
        {
            await foreach (var device in controller.GetDevices(optionsWithDriver, cts.Token))
            {
                if (!cts.IsCancellationRequested)
                {
                    var cachedIcon = _deviceCapsCache.GetCachedIcon(device.IconUri);
                    if (cachedIcon != null)
                    {
                        _deviceListViewBehavior.SetImage(device, cachedIcon);
                    }
                    else
                    {
                        var icon = await _deviceCapsCache.LoadIcon(device);
                        if (icon != null)
                        {
                            _deviceListViewBehavior.SetImage(device, icon);
                            _deviceIconList.RegenerateImages();
                        }
                    }
                    if (!DeviceSet.Contains(device))
                    {
                        DeviceList.Add(device);
                        DeviceSet.Add(device);
                    }
                    UpdateDevices(false);
                }
            }
            if (AllowAlwaysAsk && DeviceDriver is Driver.Wia or Driver.Twain)
            {
                if (!cts.IsCancellationRequested)
                {
                    ExtraItems.Add(AlwaysAskMarker);
                    UpdateDevices(false);
                }
            }
            if (!cts.IsCancellationRequested)
            {
                _spinnerVis.IsVisible = false;
                _statusIconName = DeviceList.Count > 0 ? "accept_small" : "exclamation_small";
                UpdateStatusIcon();
                _statusLabel.Text = DeviceList.Count switch
                {
                    > 1 => string.Format(UiStrings.DevicesFound, DeviceList.Count),
                    1 => UiStrings.DeviceFoundSingular,
                    _ => UiStrings.NoDevicesFound
                };
            }
        }
        catch (Exception ex)
        {
            if (!cts.IsCancellationRequested)
            {
                _spinnerVis.IsVisible = false;
                _statusIconName = "exclamation_small";
                UpdateStatusIcon();
                _statusLabel.Text = ex.Message;
            }
        }
        finally
        {
            if (!cts.IsCancellationRequested)
            {
                _activeQuery = null;
            }
        }
    }

    private void UpdateDevices(bool clear)
    {
        _deviceIconList.SetItems(DeviceList!.Concat(ExtraItems!));
        if (clear)
        {
            _deviceTextList.Items.Clear();
        }
        foreach (var device in ExtraItems!)
        {
            if (_deviceTextList.Items.All(x => x.Key != device.ID))
            {
                _deviceTextList.Items.Add(new ListItem
                {
                    Key = device.ID,
                    Text = device.Name
                });
            }
        }
        foreach (var device in DeviceList!.Skip(_deviceTextList.Items.Count - ExtraItems.Count))
        {
            _deviceTextList.Items.Insert(_deviceTextList.Items.Count - ExtraItems.Count, new ListItem
            {
                Key = device.ID,
                Text = device.Name
            });
        }
    }

    public ScanOptions? ScanOptions { get; set; }

    public bool AllowAlwaysAsk { get; set; }

    private List<ScanDevice>? DeviceList { get; set; }

    private HashSet<ScanDevice>? DeviceSet { get; set; }

    private List<ScanDevice>? ExtraItems { get; set; }

    public DeviceChoice Choice { get; private set; } = DeviceChoice.None;

    private bool SelectDevice()
    {
        if (_textListVis.IsVisible)
        {
            if (_deviceTextList.SelectedValue == null)
            {
                _deviceTextList.Focus();
                return false;
            }
            Choice = DeviceChoice.ForDevice(DeviceList!.Concat(ExtraItems!)
                .First(x => x.ID == _deviceTextList.SelectedKey));
        }
        else
        {
            if (_deviceIconList.Selection.Count == 0)
            {
                _deviceIconList.Control.Focus();
                return false;
            }
            Choice = DeviceChoice.ForDevice(_deviceIconList.Selection.First());
        }
        if (Choice.Device == AlwaysAskMarker)
        {
            Choice = DeviceChoice.ForAlwaysAsk(DeviceDriver);
        }
        if (Choice.Device == ManualIpMarker)
        {
            var ipForm = FormFactory.Create<ManualIpForm>();
            ipForm.ShowModal();
            Choice = ipForm.Device != null ? DeviceChoice.ForDevice(ipForm.Device) : DeviceChoice.None;
            return ipForm.Result;
        }
        return true;
    }
}