QuickImage – Rev 19

Subversion Repositories:
Rev:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
using Configuration;
using ImageMagick;
using ImageMagick.Factories;
using Microsoft.WindowsAPICodePack.Dialogs;
using MimeDetective.Storage;
using NetSparkleUpdater;
using NetSparkleUpdater.Enums;
using NetSparkleUpdater.SignatureVerifiers;
using NetSparkleUpdater.UI.WinForms;
using QuickImage.Database;
using QuickImage.ImageListViewSorters;
using QuickImage.Utilities;
using QuickImage.Utilities.Controls;
using QuickImage.Utilities.Extensions;
using QuickImage.Utilities.Serialization.Comma_Separated_Values;
using QuickImage.Utilities.Serialization.XML;
using Serilog;
using Shipwreck.Phash;
using Shipwreck.Phash.Bitmaps;
using Tesseract;
using ImageFormat = System.Drawing.Imaging.ImageFormat;

namespace QuickImage
{
    public partial class Form1 : Form
    {
        private readonly CancellationToken _cancellationToken;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private readonly ScheduledContinuation _changedConfigurationContinuation;
        private readonly FileMutex _fileMutex;
        private readonly TaskScheduler _formTaskScheduler;
        private readonly ConcurrentDictionary<string, ListViewGroup> _imageListViewGroupDictionary;
        private readonly SemaphoreSlim _imageListViewLock;
        private readonly ImageTool _imageTool;
        private readonly Progress<ImageListViewItemProgress<ListViewItem>> _listViewItemProgress;
        private readonly MagicMime _magicMime;
        private readonly MD5 _md5;
        private readonly LogMemorySink _memorySink = new LogMemorySink();
        private readonly QuickImageDatabase _quickImageDatabase;

        private readonly Progress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>>
            _quickImageListViewProgress;

        private readonly Random _random;
        private readonly ScheduledContinuation _searchScheduledContinuation;
        private readonly ConcurrentDictionary<string, ListViewItem> _searchStore;
        private readonly ScheduledContinuation _selectionScheduledContinuation;
        private readonly ScheduledContinuation _sortScheduledContinuation;
        private readonly SparkleUpdater _sparkle;
        private AutoCompleteStringCollection _tagAutoCompleteStringCollection;
        private TagListViewSorter _tagListViewSorter;

        private readonly TagManager _tagManager;
        private AboutForm _aboutForm;
        private CancellationToken _combinedSearchSelectionCancellationToken;
        private CancellationToken _combinedSelectionCancellationToken;
        private EditorForm _editorForm;
        private CancellationTokenSource _linkedSearchCancellationTokenSource;
        private CancellationTokenSource _linkedSelectionCancellationTokenSource;
        private PreviewForm _previewForm;
        private QuickImageSearchParameters _quickImageSearchParameters;
        private QuickImageSearchType _quickImageSearchType;
        private RenameForm _renameForm;
        private CancellationToken _searchCancellationToken;
        private CancellationTokenSource _searchCancellationTokenSource;
        private CancellationToken _selectionCancellationToken;
        private CancellationTokenSource _selectionCancellationTokenSource;
        private SettingsForm _settingsForm;
        private ViewLogsForm _viewLogsForm;

        /// <summary>
        /// The operation to perform on the menu item and descendants.
        /// </summary>
        public enum MenuItemsToggleOperation
        {
            NONE,
            ENABLE,
            DISABLE
        }

        private Configuration.Configuration Configuration { get; set; }

        public bool MemorySinkEnabled { get; set; } = true;

        private Form1()
        {
            InitializeComponent();

            _fileMutex = new FileMutex();
            _magicMime = new MagicMime(_fileMutex);

            _cancellationTokenSource = new CancellationTokenSource();
            _cancellationToken = _cancellationTokenSource.Token;
            _searchScheduledContinuation = new ScheduledContinuation();
            _selectionScheduledContinuation = new ScheduledContinuation();
            _sortScheduledContinuation = new ScheduledContinuation();
            _md5 = new MD5CryptoServiceProvider();
            _searchStore = new ConcurrentDictionary<string, ListViewItem>();
            _imageListViewLock = new SemaphoreSlim(1, 1);
            _quickImageSearchType = QuickImageSearchType.Any;
            _formTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
            _imageListViewGroupDictionary = new ConcurrentDictionary<string, ListViewGroup>();
            _changedConfigurationContinuation = new ScheduledContinuation();
            _random = new Random();

            _listViewItemProgress = new Progress<ImageListViewItemProgress<ListViewItem>>();
            _quickImageListViewProgress =
                new Progress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>>();

            _quickImageDatabase = new QuickImageDatabase(_cancellationToken);
            _tagManager = new TagManager(_fileMutex);
            _imageTool = new ImageTool();

            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .WriteTo.Conditional(condition => MemorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
                .WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
                    rollingInterval: RollingInterval.Day)
                .CreateLogger();

            // Start application update.
            var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
            var icon = Icon.ExtractAssociatedIcon(manifestModuleName);

            _sparkle = new SparkleUpdater("https://quickimage.grimore.org/update/appcast.xml",
                new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
            {
                UIFactory = new UIFactory(icon),
                RelaunchAfterUpdate = true
            };

            _sparkle.StartLoop(true, true);
        }

        public Form1(Mutex mutex) : this()
        {
        }

        private async Task SortImageListView(IComparer<ListViewItem> comparer)
        {
            var taskCompletionSources = new[] { new TaskCompletionSource<object>(), new TaskCompletionSource<object>() };

            try
            {
                await _imageListViewLock.WaitAsync(_cancellationToken);
            }
            catch
            {
                return;
            }

            toolStripStatusLabel1.Text = "Sorting...";
            toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
            toolStripProgressBar1.MarqueeAnimationSpeed = 30;

            try
            {
                var images = imageListView.Items.OfType<ListViewItem>().ToList();
                if (images.Count == 0) return;

                imageListView.Items.Clear();

#pragma warning disable CS4014
                Task.Factory.StartNew(() =>
#pragma warning restore CS4014
                {
                    images.Sort(comparer);
                    taskCompletionSources[0].TrySetResult(new { });
                }, _cancellationToken);

                await taskCompletionSources[0].Task;

                imageListView.InvokeIfRequired(view =>
                {
                    view.BeginUpdate();
                    foreach (var item in images)
                    {
                        var directoryName = Path.GetDirectoryName(item.Name);
                        if (!_imageListViewGroupDictionary.TryGetValue(directoryName, out var group))
                        {
                            group = new ListViewGroup(directoryName, HorizontalAlignment.Left) { Name = directoryName };
                            _imageListViewGroupDictionary.TryAdd(directoryName, group);
                        }

                        item.Group = group;
                    }

                    view.Items.AddRange(images.ToArray());
                    view.EndUpdate();
                    taskCompletionSources[1].TrySetResult(new { });
                });

                await taskCompletionSources[1].Task;
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Failed to sort.");
            }
            finally
            {
                this.InvokeIfRequired(form =>
                {
                    toolStripStatusLabel1.Text = "Sorting complete.";

                    form.toolStripProgressBar1.MarqueeAnimationSpeed = 0;
                });

                _imageListViewLock.Release();
            }
        }

        private async Task BalanceTags(IEnumerable<ListViewItem> items)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();
            var count = enumerable.Length;

            var bufferBlock = new BufferBlock<(string Name, ConcurrentBag<string> Tags)>(new DataflowBlockOptions{ CancellationToken = _cancellationToken });
            var broadcastBlock = new BroadcastBlock<(string Name, ConcurrentBag<string> Tags)>(x => x, new DataflowBlockOptions { CancellationToken = _cancellationToken });
            var databaseActionBlock = new ActionBlock<(string Name, ConcurrentBag<string> Tags)>(
                async file =>
                {
                    await _quickImageDatabase.AddTagsAsync(file.Name, file.Tags, _cancellationToken);
                    await _tagManager.AddIptcKeywords(file.Name, file.Tags, _cancellationToken);
                }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
            var taggingActionBlock = new ActionBlock<(string Name, ConcurrentBag<string> Tags)>(file =>
                {
                    foreach (var tag in file.Tags)
                    {
                        if (tagListView.Items.ContainsKey(tag))
                        {
                            tagListView.BeginUpdate();
                            tagListView.Items[tag].Checked = true;
                            tagListView.EndUpdate();
                            return Task.CompletedTask;
                        }

                        tagListView.BeginUpdate();
                        tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                        tagListView.Items[tag].Checked = true;
                        tagListView.EndUpdate();
                    }

                    return Task.CompletedTask;
                },
                new ExecutionDataflowBlockOptions
                { CancellationToken = _cancellationToken, TaskScheduler = _formTaskScheduler });

            using var bufferBroadcastLink =
                bufferBlock.LinkTo(broadcastBlock, new DataflowLinkOptions { PropagateCompletion = true });
            using var broadcastDatabaseLink = broadcastBlock.LinkTo(databaseActionBlock,
                new DataflowLinkOptions { PropagateCompletion = true });
            using var broadcastTaggingLink = broadcastBlock.LinkTo(taggingActionBlock,
                new DataflowLinkOptions { PropagateCompletion = true });

            try
            {
                this.InvokeIfRequired(form => { form.toolStripStatusLabel1.Text = "Balancing tags..."; });

                var tags = new ConcurrentBag<string>();
                foreach (var item in enumerable)
                {
                    await foreach (var tag in _quickImageDatabase.GetTags(item.Name, _cancellationToken).WithCancellation(_cancellationToken))
                    {
                        tags.Add(tag);
                    }
                }

                var tasks = new List<Task>();
                foreach (var item in enumerable)
                {
                    tasks.Add(bufferBlock.SendAsync((item.Name, Tags: tags), _cancellationToken));
                }

                await Task.WhenAll(tasks);

                bufferBlock.Complete();

                await bufferBlock.Completion;
                await databaseActionBlock.Completion;
                await taggingActionBlock.Completion;

                this.InvokeIfRequired(form => { form.toolStripStatusLabel1.Text = "Tags balanced."; });
            }
            catch (Exception exception)
            {
                Log.Warning(exception, "Could not balance tags.");

                this.InvokeIfRequired(form => { form.toolStripStatusLabel1.Text = "Error balancing tags..."; });
            }
        }

        private void SelectTags(IEnumerable<ListViewItem> items)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();
            var count = enumerable.Length;

            var bufferBlock = new BufferBlock<string>();

            async void IdleHandler(object idleHandlerSender, EventArgs idleHandlerArgs)
            {
                try
                {
                    if (_combinedSelectionCancellationToken.IsCancellationRequested)
                    {
                        toolStripStatusLabel1.Text = "Selection cancelled.";
                        Application.Idle -= IdleHandler;
                        return;
                    }

                    if (!await bufferBlock.OutputAvailableAsync(_combinedSelectionCancellationToken))
                    {
                        toolStripStatusLabel1.Text = "Items selected.";
                        Application.Idle -= IdleHandler;
                        return;
                    }

                    if (!bufferBlock.TryReceive(out var tag)) return;

                    if (tagListView.Items.ContainsKey(tag))
                    {
                        tagListView.BeginUpdate();
                        tagListView.Items[tag].Checked = true;
                        tagListView.EndUpdate();
                        return;
                    }

                    tagListView.BeginUpdate();
                    tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                    tagListView.Items[tag].Checked = true;
                    tagListView.EndUpdate();
                }
                catch (OperationCanceledException)
                {
                    Application.Idle -= IdleHandler;
                }
                catch (Exception exception)
                {
                    Application.Idle -= IdleHandler;
                    Log.Warning(exception, "Failed selecting tags for image.");
                }
                finally
                {
                    _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), () =>
                    {
                        tagListView.BeginUpdate();
                        tagListView.Sort();
                        tagListView.EndUpdate();
                    }, _formTaskScheduler, _cancellationToken);
                }
            }

            if (count == 0)
            {
                if (_selectionCancellationTokenSource != null)
                {
                    _selectionCancellationTokenSource.Cancel();
                }

                tagListView.BeginUpdate();
                foreach (var item in tagListView.CheckedItems.OfType<ListViewItem>())
                {
                    item.Checked = false;
                }

                tagListView.EndUpdate();
                return;
            }

            if (_selectionCancellationTokenSource != null)
            {
                _selectionCancellationTokenSource.Cancel();
            }

            _selectionCancellationTokenSource = new CancellationTokenSource();
            _selectionCancellationToken = _selectionCancellationTokenSource.Token;
            _linkedSelectionCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _selectionCancellationToken);
            _combinedSelectionCancellationToken = _linkedSelectionCancellationTokenSource.Token;
            _combinedSelectionCancellationToken.Register(() => { Application.Idle -= IdleHandler; });

            _selectionScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), async () =>
            {
                try
                {
                    tagListView.InvokeIfRequired(view =>
                    {
                        view.BeginUpdate();

                        foreach (var item in view.CheckedItems.OfType<ListViewItem>())
                        {
                            item.Checked = false;
                        }

                        view.EndUpdate();
                    });

                    this.InvokeIfRequired(form =>
                    {
                        form.toolStripStatusLabel1.Text = "Selecting items...";
                        Application.Idle += IdleHandler;
                    });

                    var tasks = new ConcurrentBag<Task>();
                    foreach (var item in enumerable)
                    {
                        await foreach (var tag in _quickImageDatabase.GetTags(item.Name, _combinedSelectionCancellationToken).WithCancellation(_combinedSelectionCancellationToken))
                        {
                            tasks.Add(bufferBlock.SendAsync(tag, _combinedSelectionCancellationToken));
                        }
                    }

                    await Task.WhenAll(tasks);
                    bufferBlock.Complete();
                }
                catch (Exception exception)
                {
                    this.InvokeIfRequired(form =>
                    {
                        toolStripStatusLabel1.Text = "Failed selecting items.";
                        Application.Idle -= IdleHandler;
                    });

                    Log.Warning(exception, "Could not select items.");
                }
            }, _combinedSelectionCancellationToken);
        }

        private void RelocateTo(IEnumerable<ListViewItem> items, string destinationDirectory,
            CancellationToken cancellationToken)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = enumerable.Length;
            toolStripProgressBar1.Value = 0;

            var transformBlock =
                new TransformBlock<(ListViewItem Item, string File), (ListViewItem Item, Database.QuickImage Image)>(
                    async tuple =>
                    {
                        try
                        {
                            var (item, path) = tuple;

                            var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);

                            var name = Path.GetFileName(item.Name);
                            var file = Path.Combine(path, $"{name}");

                            if (!await _quickImageDatabase.SetFile(item.Name, file, cancellationToken))
                                return (null, null);

                            var newImage = new Database.QuickImage(file, image.Hash, image.Tags, image.Thumbnail);

                            return (Item: item, Image: newImage);
                        }
                        catch
                        {
                            return (null, null);
                        }
                    }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });

            var actionBlock = new ActionBlock<(ListViewItem Item, Database.QuickImage Image)>(async tuple =>
                {
                    imageListView.BeginUpdate();
                    try
                    {
                        var (item, image) = tuple;

                        if (item == null || image == null) return;

                        imageListView.Items.Remove(item);

                        var fileInfo = new FileInfo(image.File);

                        if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
                        {
                            group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
                            { Name = fileInfo.DirectoryName };
                            _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
                            imageListView.Groups.Add(group);
                        }

                        largeImageList.Images.RemoveByKey(item.Name);
                        largeImageList.Images.Add(image.File, image.Thumbnail);

                        var listViewItem = imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
                        {
                            Name = image.File,
                            ImageKey = image.File,
                            Text = fileInfo.Name,
                            Group = group
                        });
                        imageListView.EnsureVisible(listViewItem.Index);

                        toolStripStatusLabel1.Text = "Relocating image directory...";
                        toolStripProgressBar1.Increment(1);
                    }
                    catch
                    {
                        toolStripStatusLabel1.Text = "Failed to relocate image to directory...";
                        toolStripProgressBar1.Increment(1);
                    }
                    finally
                    {
                        imageListView.EndUpdate();
                    }
                },
                new ExecutionDataflowBlockOptions
                { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });

            toolStripStatusLabel1.Text = "Relocating images...";

#pragma warning disable CS4014
            Task.Factory.StartNew(async () =>
#pragma warning restore CS4014
            {
                try
                {
                    await _imageListViewLock.WaitAsync(cancellationToken);
                }
                catch
                {
                    return;
                }

                try
                {
                    using var _1 = transformBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true });

                    var tasks = new ConcurrentBag<Task>();
                    foreach (var item in enumerable)
                    {
                        if (cancellationToken.IsCancellationRequested)
                        {
                            return;
                        }

                        var task = transformBlock.SendAsync((Item: item, File: destinationDirectory), cancellationToken);
                        tasks.Add(task);
                    }

                    await Task.WhenAll(tasks);
                    transformBlock.Complete();
                    await actionBlock.Completion;
                }
                finally
                {
                    _imageListViewLock.Release();
                }
            }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        private void RemoveImages(IEnumerable<ListViewItem> items, CancellationToken cancellationToken)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = enumerable.Length;
            toolStripProgressBar1.Value = 0;

            var bufferBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions { CancellationToken = cancellationToken });

            var actionBlock = new ActionBlock<ListViewItem>(listViewItem =>
                {
                    toolStripStatusLabel1.Text = $"Unloading image {listViewItem.Name}";
                    imageListView.Items.Remove(listViewItem);
                    toolStripProgressBar1.Increment(1);
                },
                new ExecutionDataflowBlockOptions{ CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });

            toolStripStatusLabel1.Text = "Unloading images...";

#pragma warning disable CS4014
            Task.Factory.StartNew(async () =>
#pragma warning restore CS4014
            {
                try
                {
                    await _imageListViewLock.WaitAsync(cancellationToken);
                }
                catch
                {
                    return;
                }

                try
                {
                    using var _ = bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true });

                    foreach (var item in enumerable)
                    {
                        if (cancellationToken.IsCancellationRequested) return;

                        try
                        {
                            if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken))
                            {
                                continue;
                            }

                            await bufferBlock.SendAsync(item, cancellationToken);
                        }
                        catch
                        {
                            // ignored
                        }
                    }

                    bufferBlock.Complete();
                    await actionBlock.Completion;
                }
                finally
                {
                    _imageListViewLock.Release();
                }
            }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        public async Task RemoveMissingAsync(IEnumerable<string> groups, CancellationToken cancellationToken)
        {
            var enumerable = groups as string[] ?? groups.ToArray();

            var bufferBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions
            { CancellationToken = cancellationToken });

            var transformBlock = new TransformBlock<ListViewItem, ListViewItem>(listViewItem =>
                {
                    if (File.Exists(listViewItem.Name))
                    {
                        toolStripStatusLabel1.Text = $"Image {listViewItem.Name} exists.";
                        return null;
                    }

                    toolStripStatusLabel1.Text = $"Deleting image {listViewItem.Name}";
                    imageListView.Items.Remove(listViewItem);

                    return listViewItem;
                },
                new ExecutionDataflowBlockOptions
                { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });

            var actionBlock = new ActionBlock<ListViewItem>(async item =>
            {
                if (item == null) 
                {
                    return;
                }

                try
                {
                    await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken);
                }
                catch
                {
                    // ignored
                }
            });

            using var _1 = bufferBlock.LinkTo(transformBlock, new DataflowLinkOptions { PropagateCompletion = true });
            using var _2 = transformBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true });
            using var _3 = transformBlock.LinkTo(DataflowBlock.NullTarget<ListViewItem>());

            toolStripStatusLabel1.Text = "Removing missing images...";
            try
            {
                await _imageListViewLock.WaitAsync(cancellationToken);
            }
            catch
            {
                return;
            }

            toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
            toolStripProgressBar1.MarqueeAnimationSpeed = 30;

            try
            {
                var tasks = new ConcurrentBag<Task>();
                foreach (var group in enumerable)
                {
                    foreach (var item in imageListView.Items.OfType<ListViewItem>())
                    {
                        if (string.Equals(item.Group.Name, group, StringComparison.OrdinalIgnoreCase))
                        {
                            tasks.Add(bufferBlock.SendAsync(item, cancellationToken));
                        }
                    }
                }

                bufferBlock.Complete();

                await Task.WhenAll(tasks);

                await transformBlock.Completion.ContinueWith(_ => { actionBlock.Complete(); }, cancellationToken);

                await actionBlock.Completion;
            }
            finally
            {
                toolStripProgressBar1.MarqueeAnimationSpeed = 0;
                _imageListViewLock.Release();
                toolStripStatusLabel1.Text = "Done removing images.";
            }
        }
        
        private async Task LoadDataObjectAsync(IDataObject dataObject)
        {
            var inputBlock = new BufferBlock<(string File, Stream Data, Definition Mime)>(
                new ExecutionDataflowBlockOptions
                { CancellationToken = _cancellationToken });

            var transformBlock = new TransformBlock<(string File, Stream Data, Definition Mime), string>(async tuple =>
            {
                try
                {
                    var (_, data, _) = tuple;
                    data.Position = 0L;

                    var path = Path.GetTempPath();
                    var name = Path.GetTempFileName();

                    using var memoryStream = new MemoryStream();
                    switch (Configuration.InboundDragDrop.DragDropConvertType)
                    {
                        case "image/jpeg":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(data, MagickFormat.Jpeg, _cancellationToken);
                                await convertStream.CopyToAsync(memoryStream);
                                name = Path.ChangeExtension(name, "jpg");
                                break;
                            }
                        case "image/png":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(data, MagickFormat.Png, _cancellationToken);
                                await convertStream.CopyToAsync(memoryStream);
                                name = Path.ChangeExtension(name, "png");
                                break;
                            }
                        case "image/bmp":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(data, MagickFormat.Bmp, _cancellationToken);
                                await convertStream.CopyToAsync(memoryStream);
                                name = Path.ChangeExtension(name, "bmp");
                                break;
                            }
                        case "image/gif":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(data, MagickFormat.Gif, _cancellationToken);
                                await convertStream.CopyToAsync(memoryStream);
                                name = Path.ChangeExtension(name, "gif");
                                break;
                            }
                        // create a copy for files that do not have to be converted
                        default:
                            throw new ArgumentException(
                                "Unsupported conversion type for image.");
                    }

                    var destinationFile = Path.Combine(path, name);

                    memoryStream.Position = 0L;
                    await Miscellaneous.CopyFileAsync(memoryStream, destinationFile, _cancellationToken);

                    return destinationFile;
                }
                catch (Exception exception)
                {
                    Log.Warning(exception, "Unable to convert input file.");
                    return null;
                }
            }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

            var copyTransformBlock = new TransformBlock<(string File, Stream Data, Definition Mime), string>(
                async tuple =>
                {
                    try
                    {
                        var (_, data, mime) = tuple;
                        data.Position = 0L;

                        var path = Path.GetTempPath();
                        var name = Path.GetTempFileName();
                        var extension = mime.File.Extensions.FirstOrDefault();
                        name = Path.ChangeExtension(name, extension);
                        var destinationFile = Path.Combine(path, name);

                        await Miscellaneous.CopyFileAsync(data, destinationFile, _cancellationToken);

                        return destinationFile;
                    }
                    catch (Exception exception)
                    {
                        Log.Warning(exception, "Unable to create a copy of the file.");

                        return null;
                    }
                }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

            var importTransformBlock =
                new TransformBlock<(string File, Stream Data, Definition Mime), string>(
                    tuple => Task.FromResult(tuple.File),
                    new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

            var outputBlock = new BufferBlock<string>(new ExecutionDataflowBlockOptions
            { CancellationToken = _cancellationToken });

            using var _1 = inputBlock.LinkTo(transformBlock,
                tuple => Configuration.InboundDragDrop.ConvertOnDragDrop &&
                         Configuration.SupportedFormats.IsSupportedImage(tuple.Mime.File.MimeType) &&
                         !Configuration.InboundDragDrop.DragDropConvertExclude
                             .IsExcludedImage(tuple.Mime.File.MimeType));
            using var _2 = transformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
            using var _3 = transformBlock.LinkTo(DataflowBlock.NullTarget<string>());

            using var _4 = inputBlock.LinkTo(copyTransformBlock, tuple => Configuration.InboundDragDrop.CopyOnDragDrop);
            using var _5 = copyTransformBlock.LinkTo(outputBlock);
            using var _6 = copyTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());

            using var _7 = inputBlock.LinkTo(importTransformBlock,
                tuple => !Configuration.InboundDragDrop.ConvertOnDragDrop &&
                         !Configuration.InboundDragDrop.CopyOnDragDrop);
            using var _8 = importTransformBlock.LinkTo(outputBlock);
            using var _9 = importTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());

            var tasks = new List<Task>();
            await foreach (var (file, data, mime) in GetDragDropFiles(dataObject, _magicMime, _cancellationToken))
            {
                if (!Configuration.SupportedFormats.IsSupported(mime.File.MimeType))
                {
                    continue;
                }

                tasks.Add(inputBlock.SendAsync((File: file, Data: data, Mime: mime), _cancellationToken));
            }

            await Task.WhenAll(tasks);
            inputBlock.Complete();

            await inputBlock.Completion.ContinueWith(_ =>
            {
                transformBlock.Complete();
                copyTransformBlock.Complete();
                importTransformBlock.Complete();
            }, _cancellationToken);

            await Task.WhenAll(transformBlock.Completion, copyTransformBlock.Completion, importTransformBlock.Completion).ContinueWith(_ => { outputBlock.Complete(); }, _cancellationToken);

            var set = new HashSet<string>();
            while (await outputBlock.OutputAvailableAsync(_cancellationToken))
            {
                if (!outputBlock.TryReceiveAll(out var items))
                {
                    continue;
                }

                set.UnionWith(items);
            }

            await LoadFilesAsync(set, _magicMime, _cancellationToken);
        }

        private async Task LoadFilesAsync(IEnumerable<string> files, MagicMime magicMime,
            CancellationToken cancellationToken)
        {
            var enumerable = files as string[] ?? files.ToArray();

            var updateImageListViewActionBlock = new ActionBlock<Database.QuickImage>(
                async tuple =>
                {
                    try
                    {
                        var (file, tags, thumbnail) = (tuple.File, tuple.Tags, tuple.Thumbnail);

                        if (imageListView.Items.ContainsKey(file))
                        {
                            toolStripStatusLabel1.Text = $"File {file} already exits.";
                            return;
                        }

                        if (!largeImageList.Images.ContainsKey(file))
                        {
                            largeImageList.Images.Add(file, thumbnail);
                        }

                        var fileInfo = new FileInfo(file);
                        if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
                        {
                            group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left) { Name = fileInfo.DirectoryName };
                            _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
                            imageListView.Groups.Add(group);
                        }

                        var imageListViewItem = new ListViewItem(file) { Name = file, ImageKey = file, Text = fileInfo.Name, Group = group };
                        imageListView.Items.Add(imageListViewItem);
                        imageListView.EnsureVisible(imageListViewItem.Index);

                        toolStripStatusLabel1.Text = $"Added file {file} to the list.";

                        foreach (var tag in tags)
                        {
                            if (!_tagAutoCompleteStringCollection.Contains(tag))
                            {
                                _tagAutoCompleteStringCollection.Add(tag);
                            }

                            if (tagListView.Items.ContainsKey(tag))
                            {
                                continue;
                            }

                            tagListView.BeginUpdate();
                            tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                            tagListView.EndUpdate();
                        }
                    }
                    catch (Exception exception)
                    {
                        Log.Warning(exception, "Could not update image list view.");
                    }
                },
                new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });

            var fileInputBlock = new BufferBlock<string>(new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });

            var updateImageTagsTransformBlock = new TransformBlock<string, Database.QuickImage>(async file =>
            {
                try
                {
                    var tags = new HashSet<string>();

                    var databaseTags = await _quickImageDatabase.GetTags(file, cancellationToken).ToArrayAsync(cancellationToken);

                    tags.UnionWith(databaseTags);

                    var mime = await magicMime.GetMimeType(file, cancellationToken);

                    if (Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        await foreach (var iptcTag in _tagManager.GetIptcKeywords(file, cancellationToken))
                        {
                            tags.UnionWith(new[] { iptcTag });
                        }
                    }

                    await _quickImageDatabase.AddTagsAsync(file, tags, cancellationToken);

                    return await _quickImageDatabase.GetImageAsync(file, cancellationToken);
                }
                catch (Exception exception)
                {
                    Log.Warning(exception, $"Could not add {file} to database.");

                    return null;
                }
            }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });

            var createImageTransformBlock = new TransformBlock<string, Database.QuickImage>(async file =>
            {
                try
                {
                    var tags = Array.Empty<string>();

                    using var imageCollection = new MagickImageCollection(file, new MagickReadSettings { FrameIndex = 0, FrameCount = 1 });

                    var imageFrame = imageCollection[0];

                    var mime = await magicMime.GetMimeType(file, cancellationToken);

                    if (Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        var iptcTags = _tagManager.GetIptcKeywords(imageFrame);

                        tags = iptcTags.ToArray();
                    }

                    var buffer = imageFrame.ToByteArray(MagickFormat.Bmp);
                    using var bitmapMemoryStream = new MemoryStream(buffer);

                    bitmapMemoryStream.Position = 0L;
                    using var hashBitmap = (Bitmap)Image.FromStream(bitmapMemoryStream);
                    var hash = ImagePhash.ComputeDigest(hashBitmap.ToBitmap().ToLuminanceImage());

                    bitmapMemoryStream.Position = 0L;
                    using var thumbnailBitmap = await CreateThumbnail(bitmapMemoryStream, 128, 128, cancellationToken);
                    var thumbnail = new Bitmap(thumbnailBitmap);
                    thumbnailBitmap.Dispose();

                    await _quickImageDatabase.AddImageAsync(file, hash, tags, thumbnail, cancellationToken);

                    return new Database.QuickImage(file, hash, tags, thumbnail);
                }
                catch (Exception exception)
                {
                    Log.Warning(exception, $"Could not add {file} to database.");

                    return null;
                }
            });

            using var _2 = fileInputBlock.LinkTo(updateImageTagsTransformBlock, file =>
            {
                try
                {
                    return _quickImageDatabase.Exists(file, cancellationToken);
                }
                catch (Exception exception)
                {
                    Log.Warning(exception, $"Could not query database for file {file}");
                    return false;
                }
            });
            using var _4 = updateImageTagsTransformBlock.LinkTo(updateImageListViewActionBlock, new DataflowLinkOptions { PropagateCompletion = true });
            using var _5 =
                updateImageTagsTransformBlock.LinkTo(DataflowBlock.NullTarget<Database.QuickImage>(), image =>
                {
                    var r = image == null;

                    return r;
                });

            using var _3 = fileInputBlock.LinkTo(createImageTransformBlock, new DataflowLinkOptions { PropagateCompletion = true });
            using var _6 = createImageTransformBlock.LinkTo(updateImageListViewActionBlock, new DataflowLinkOptions { PropagateCompletion = true });
            using var _7 =
                createImageTransformBlock.LinkTo(DataflowBlock.NullTarget<Database.QuickImage>(), image =>
                {
                    var r = image == null;

                    return r;
                });

            toolStripStatusLabel1.Text = "Loading images...";
            try
            {
                await _imageListViewLock.WaitAsync(cancellationToken);
            }
            catch
            {
                return;
            }

            try
            {
                toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
                toolStripProgressBar1.MarqueeAnimationSpeed = 30;

                var tasks = new ConcurrentBag<Task>();
                foreach (var item in enumerable)
                {
                    await foreach (var entry in GetFilesAsync(item, Configuration, magicMime, cancellationToken).WithCancellation(cancellationToken))
                    {
                        tasks.Add(fileInputBlock.SendAsync(entry, cancellationToken));
                    }
                }

                await Task.WhenAll(tasks);

                fileInputBlock.Complete();

                await updateImageListViewActionBlock.Completion;
            }
            finally
            {
                toolStripProgressBar1.MarqueeAnimationSpeed = 0;
                _imageListViewLock.Release();

                toolStripStatusLabel1.Text = "Done loading images.";
            }
        }

        private async Task OcrImagesAsync(IEnumerable<ListViewItem> items, CancellationToken cancellationToken)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = enumerable.Length;
            toolStripProgressBar1.Value = 0;

            void QuickImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
                        toolStripStatusLabel1.Text =
                            $"Added {imageListViewItemProgressSuccess.Tags.Count()} to image {imageListViewItemProgressSuccess.Item.Name} using OCR.";
                        foreach (var tag in imageListViewItemProgressSuccess.Tags)
                        {
                            if (tagListView.Items.ContainsKey(tag))
                            {
                                tagListView.BeginUpdate();
                                tagListView.Items[tag].Checked = true;
                                tagListView.EndUpdate();
                                continue;
                            }

                            tagListView.BeginUpdate();
                            tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                            tagListView.Items[tag].Checked = true;
                            tagListView.EndUpdate();
                        }

                        break;
                    case ImageListViewItemProgressFailure<ListViewItem> _:
                        break;
                }

                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _listViewItemProgress.ProgressChanged -= QuickImageListViewItemProgress;
                }
            }

            toolStripStatusLabel1.Text = "Settings text in images to tags using OCR...";
            try
            {
                await _imageListViewLock.WaitAsync(cancellationToken);
            }
            catch
            {
                return;
            }

            _listViewItemProgress.ProgressChanged += QuickImageListViewItemProgress;
            try
            {
                await OcrImages(enumerable, _listViewItemProgress, cancellationToken);
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Error while scanning text in images.");

                _listViewItemProgress.ProgressChanged -= QuickImageListViewItemProgress;
                _imageListViewLock.Release();
            }
        }

        private async Task ConvertImagesAsync(IEnumerable<ListViewItem> items, string extension,
            CancellationToken cancellationToken)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = enumerable.Length;
            toolStripProgressBar1.Value = 0;

            void QuickImageListViewItemProgress(object sender, ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)> imageListViewItemProgressSuccess:
                        if (imageListViewItemProgressSuccess.Item is { } tuple)
                        {
                            var (item, image) = tuple;

                            imageListView.BeginUpdate();
                            try
                            {
                                imageListView.Items.Remove(item);

                                var fileInfo = new FileInfo(image.File);

                                if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
                                {
                                    group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
                                    { Name = fileInfo.DirectoryName };
                                    _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
                                    imageListView.Groups.Add(group);
                                }

                                largeImageList.Images.RemoveByKey(item.Name);

                                largeImageList.Images.Add(image.File, image.Thumbnail);

                                imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
                                {
                                    Name = image.File,
                                    ImageKey = image.File,
                                    Text = fileInfo.Name,
                                    Group = group
                                });
                            }
                            finally
                            {
                                imageListView.EndUpdate();
                            }
                        }

                        break;
                    case ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)> _:
                        break;
                }

                toolStripStatusLabel1.Text = "Converting images...";
                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
                    _imageListViewLock.Release();
                }
            }

            toolStripStatusLabel1.Text = "Converting images...";
            try
            {
                await _imageListViewLock.WaitAsync(cancellationToken);
            }
            catch
            {
                return;
            }

            _quickImageListViewProgress.ProgressChanged += QuickImageListViewItemProgress;
            try
            {
                await ConvertImages(enumerable, extension, _quickImageListViewProgress, cancellationToken);
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Error while converting images.");

                _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
                _imageListViewLock.Release();
            }
        }

        private async Task RenameImageAsync(ListViewItem item, string destinationFileName,
            CancellationToken cancellationToken)
        {
            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = 1;
            toolStripProgressBar1.Value = 0;

            void QuickImageListViewItemProgress(object sender, ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>
                        imageListViewItemProgressSuccess:
                        if (imageListViewItemProgressSuccess.Item is { } tuple)
                        {
                            var (item, image) = tuple;

                            imageListView.BeginUpdate();

                            try
                            {
                                imageListView.Items.Remove(item);

                                var fileInfo = new FileInfo(image.File);

                                if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
                                {
                                    group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left) { Name = fileInfo.DirectoryName };
                                    _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
                                    imageListView.Groups.Add(group);
                                }

                                largeImageList.Images.RemoveByKey(item.Name);
                                largeImageList.Images.Add(image.File, image.Thumbnail);

                                var listViewItem = imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
                                {
                                    Name = image.File,
                                    ImageKey = image.File,
                                    Text = fileInfo.Name,
                                    Group = group
                                });
                                imageListView.EnsureVisible(listViewItem.Index);
                            }
                            finally
                            {
                                imageListView.EndUpdate();
                            }
                        }

                        break;
                    case ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)> _:
                        break;
                }

                toolStripStatusLabel1.Text = "Renaming image...";
                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
                    _imageListViewLock.Release();
                }
            }

            toolStripStatusLabel1.Text = "Renaming image...";
            try
            {
                await _imageListViewLock.WaitAsync(cancellationToken);
            }
            catch
            {
                return;
            }

            _quickImageListViewProgress.ProgressChanged += QuickImageListViewItemProgress;
            try
            {
                var directoryName = Path.GetDirectoryName(item.Name);

                await RenameImage(item, Path.Combine(directoryName, destinationFileName), _quickImageListViewProgress, cancellationToken);
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Error while renaming image.");

                _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
                _imageListViewLock.Release();
            }
        }

        private async Task MoveImagesAsync(IEnumerable<ListViewItem> items, string destinationDirectory,
            CancellationToken cancellationToken)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = enumerable.Length;
            toolStripProgressBar1.Value = 0;

            void QuickImageListViewItemProgress(object sender, ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)> imageListViewItemProgressSuccess:
                        if (imageListViewItemProgressSuccess.Item is { } tuple)
                        {
                            var (item, image) = tuple;

                            imageListView.BeginUpdate();
                            try
                            {
                                imageListView.Items.Remove(item);

                                var fileInfo = new FileInfo(image.File);

                                if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
                                {
                                    group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
                                    { Name = fileInfo.DirectoryName };
                                    _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
                                    imageListView.Groups.Add(group);
                                }

                                largeImageList.Images.RemoveByKey(item.Name);
                                largeImageList.Images.Add(image.File, image.Thumbnail);

                                var listViewItem = imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
                                {
                                    Name = image.File,
                                    ImageKey = image.File,
                                    Text = fileInfo.Name,
                                    Group = group
                                });

                                imageListView.EnsureVisible(listViewItem.Index);
                            }
                            finally
                            {
                                imageListView.EndUpdate();
                            }
                        }

                        break;
                    case ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)> _:
                        break;
                }

                toolStripStatusLabel1.Text = "Moving images...";
                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
                    _imageListViewLock.Release();
                }
            }

            toolStripStatusLabel1.Text = "Moving images...";
            try
            {
                await _imageListViewLock.WaitAsync(cancellationToken);
            }
            catch
            {
                return;
            }

            _quickImageListViewProgress.ProgressChanged += QuickImageListViewItemProgress;
            try
            {
                await MoveImages(enumerable, destinationDirectory, _quickImageListViewProgress, cancellationToken);
            }
            catch
            {
                _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
                _imageListViewLock.Release();
            }
        }

        private void DeleteImages(IEnumerable<ListViewItem> items, CancellationToken cancellationToken)
        {
            var enumerable = items as ListViewItem[] ?? items.ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = enumerable.Length;
            toolStripProgressBar1.Value = 0;

            var bufferBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions
            { CancellationToken = cancellationToken });
            var actionBlock = new ActionBlock<ListViewItem>(listViewItem =>
                {
                    toolStripStatusLabel1.Text = $"Deleting image {listViewItem.Name}";
                    imageListView.Items.Remove(listViewItem);
                },
                new ExecutionDataflowBlockOptions
                { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });

            toolStripStatusLabel1.Text = "Deleting images...";

#pragma warning disable CS4014
            Task.Factory.StartNew(async () =>
#pragma warning restore CS4014
            {
                try
                {
                    await _imageListViewLock.WaitAsync(cancellationToken);
                }
                catch
                {
                    return;
                }

                try
                {
                    using var _ = bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true });

                    foreach (var item in enumerable)
                    {
                        if (cancellationToken.IsCancellationRequested)
                        {
                            return;
                        }

                        try
                        {
                            File.Delete(item.Name);

                            if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken))
                            {
                                continue;
                            }

                            await bufferBlock.SendAsync(item, cancellationToken);
                        }
                        catch
                        {
                            // ignored
                        }
                    }

                    bufferBlock.Complete();
                    await actionBlock.Completion;
                }
                finally
                {
                    _imageListViewLock.Release();
                }
            });
        }

        private async Task BeginSearch(string text)
        {
            var keywords = new Csv(text).Select(tag => tag.Trim()).Where(tag => !string.IsNullOrEmpty(tag)).ToArray();

            try
            {
                await _imageListViewLock.WaitAsync(_cancellationToken);
            }
            catch
            {
                return;
            }

            if (_selectionCancellationTokenSource != null)
            {
                _selectionCancellationTokenSource.Cancel();
            }

            imageListView.InvokeIfRequired(view => { imageListView.BeginUpdate(); });

            try
            {
                var taskCompletionSource = new TaskCompletionSource<object>();
                imageListView.InvokeIfRequired(view =>
                {
                    foreach (var item in view.Items.OfType<ListViewItem>())
                    {
                        _searchStore.TryAdd(item.Name, item);
                    }

                    view.Items.Clear();

                    taskCompletionSource.TrySetResult(new { });
                });

                await taskCompletionSource.Task;

                await foreach (var quickImage in _quickImageDatabase.Search(keywords, _quickImageSearchType, _quickImageSearchParameters, _combinedSearchSelectionCancellationToken).WithCancellation(_combinedSearchSelectionCancellationToken))
                {
                    if (!_searchStore.TryGetValue(quickImage.File, out var item))
                    {
                        continue;
                    }

                    var directoryName = Path.GetDirectoryName(item.Name);
                    if (_imageListViewGroupDictionary.TryGetValue(directoryName, out var group))
                    {
                        item.Group = group;
                    }

                    imageListView.InvokeIfRequired(view => { view.Items.Add(item); });
                }
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Error while searching.");
            }
            finally
            {
                _imageListViewLock.Release();
                imageListView.InvokeIfRequired(view => { imageListView.EndUpdate(); });
            }
        }

        private async Task EndSearch()
        {
            try
            {
                await _imageListViewLock.WaitAsync(_combinedSearchSelectionCancellationToken);
            }
            catch
            {
                return;
            }

            if (_selectionCancellationTokenSource != null)
            {
                _selectionCancellationTokenSource.Cancel();
            }

            imageListView.InvokeIfRequired(view => { imageListView.BeginUpdate(); });
            try
            {
                toolStripStatusLabel1.Text = "Restoring items.";

                var taskCompletionSource = new TaskCompletionSource<object>();

                imageListView.InvokeIfRequired(view =>
                {
                    view.BeginUpdate();
                    view.Items.Clear();
                    var restore = new List<ListViewItem>();
                    foreach (var item in _searchStore)
                    {
                        var (name, listViewItem) = (item.Key, item.Value);
                        var directoryName = Path.GetDirectoryName(name);
                        if (!_imageListViewGroupDictionary.TryGetValue(directoryName, out var group))
                        {
                            group = new ListViewGroup(directoryName, HorizontalAlignment.Left) { Name = directoryName };
                            _imageListViewGroupDictionary.TryAdd(directoryName, group);
                        }

                        listViewItem.Group = group;
                        restore.Add(listViewItem);
                    }

                    view.Items.AddRange(restore.ToArray());
                    view.EndUpdate();
                    taskCompletionSource.TrySetResult(new { });
                });

                await taskCompletionSource.Task;
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Unable to add back items after search.");
            }
            finally
            {
                _imageListViewLock.Release();
                imageListView.InvokeIfRequired(view => { imageListView.EndUpdate(); });
            }
        }

        private async Task OcrImages(IEnumerable<ListViewItem> items,
            IProgress<ImageListViewItemProgress<ListViewItem>> progress,
            CancellationToken cancellationToken)
        {
            using var engine = new TesseractEngine(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tessdata"), "eng", EngineMode.Default);
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested) return;

                try
                {
                    using var img = Pix.LoadFromFile(item.Name);
                    using var page = engine.Process(img);
                    var text = page.GetText();
                    var tags = new HashSet<string>();
                    if (string.IsNullOrEmpty(text))
                    {
                        progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
                        continue;
                    }

                    foreach (var word in Regex.Split(text, @"[^\w]+", RegexOptions.Compiled))
                    {
                        if (string.IsNullOrEmpty(word)) continue;

                        tags.UnionWith(new[] { word });
                    }

                    if (!tags.Any())
                    {
                        progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
                        continue;
                    }

                    if (!await _quickImageDatabase.AddTagsAsync(item.Name, tags, cancellationToken))
                    {
                        progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
                }
                catch (Exception exception)
                {
                    progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
                }
            }
        }

        private async Task ConvertImages(IEnumerable<ListViewItem> items, string extension,
            IProgress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>> progress,
            CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);

                    var path = Path.GetDirectoryName(item.Name);
                    var name = Path.GetFileNameWithoutExtension(item.Name);
                    var file = Path.Combine(path, $"{name}.{extension}");

                    using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write);
                    switch (extension)
                    {
                        case "jpg":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(item.Name, MagickFormat.Jpeg, _cancellationToken);
                                await convertStream.CopyToAsync(fileStream);
                                break;
                            }
                        case "png":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(item.Name, MagickFormat.Png, _cancellationToken);
                                await convertStream.CopyToAsync(fileStream);
                                break;
                            }
                        case "bmp":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(item.Name, MagickFormat.Bmp, _cancellationToken);
                                await convertStream.CopyToAsync(fileStream);
                                break;
                            }
                        case "gif":
                            {
                                using var convertStream =
                                    await _imageTool.ConvertTo(item.Name, MagickFormat.Gif, _cancellationToken);
                                await convertStream.CopyToAsync(fileStream);
                                break;
                            }
                    }

                    if (!await _quickImageDatabase.RemoveImageAsync(image, cancellationToken))
                    {
                        progress.Report(
                            new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                                (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    File.Delete(item.Name);

                    var newImage = new Database.QuickImage(file, image.Hash, image.Tags, image.Thumbnail);
                    if (!await _quickImageDatabase.AddImageAsync(newImage, cancellationToken))
                    {
                        progress.Report(
                            new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                                (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    progress.Report(
                        new ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>((
                            Item: item, Image: newImage)));
                }
                catch (Exception exception)
                {
                    progress.Report(
                        new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                            (Item: item, Image: null), exception));
                }
            }
        }

        private async Task RenameImage(ListViewItem item, string destinationFileName,
            IProgress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>> progress,
            CancellationToken cancellationToken)
        {
            try
            {
                await Miscellaneous.CopyFileAsync(item.Name, destinationFileName, cancellationToken);

                File.Delete(item.Name);

                var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);

                if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken))
                {
                    progress.Report(
                        new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                            (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                    return;
                }

                var destinationImage =
                    new Database.QuickImage(destinationFileName, image.Hash, image.Tags, image.Thumbnail);

                if (!await _quickImageDatabase.AddImageAsync(destinationImage, cancellationToken))
                {
                    progress.Report(
                        new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                            (Item: item, Image: destinationImage),
                            new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                    return;
                }

                progress.Report(
                    new ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>(
                        (Item: item, Image: destinationImage)));
            }
            catch (Exception exception)
            {
                progress.Report(
                    new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                        (Item: item, Image: null), exception));
            }
        }

        private async Task MoveImages(IEnumerable<ListViewItem> items, string destinationDirectory,
            IProgress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>> progress,
            CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var fileName = Path.GetFileName(item.Name);
                    var destinationFile = Path.Combine(destinationDirectory, fileName);
                    await Miscellaneous.CopyFileAsync(item.Name, destinationFile, cancellationToken);
                    File.Delete(item.Name);

                    var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);

                    if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken))
                    {
                        progress.Report(
                            new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                                (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    var destinationImage =
                        new Database.QuickImage(destinationFile, image.Hash, image.Tags, image.Thumbnail);

                    if (!await _quickImageDatabase.AddImageAsync(destinationImage, cancellationToken))
                    {
                        progress.Report(
                            new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                                (Item: item, Image: destinationImage),
                                new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    progress.Report(
                        new ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>(
                            (Item: item, Image: destinationImage)));
                }
                catch (Exception exception)
                {
                    progress.Report(
                        new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
                            (Item: item, Image: null), exception));
                }
            }
        }

        private async Task GetTags(IReadOnlyList<ListViewItem> items,
            IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var tags = await _quickImageDatabase.GetTags(item.Name, cancellationToken)
                        .ToArrayAsync(cancellationToken);

                    progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
                }
                catch (Exception exception)
                {
                    progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
                }
            }
        }

        private async Task BalanceImageTags(IReadOnlyList<ListViewItem> items, MagicMime magicMime,
            IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var tags = await _quickImageDatabase.GetTags(item.Name, cancellationToken).ToArrayAsync(cancellationToken);

                    var mime = await magicMime.GetMimeType(item.Name, cancellationToken);

                    if (Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        if (!await _tagManager.AddIptcKeywords(item.Name, tags, cancellationToken))
                        {
                            progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                            continue;
                        }
                    }

                    var merge = new HashSet<string>(tags);
                    if (Configuration.SupportedFormats.Images.Image.Contains(mime))
                        await foreach (var iptcTag in _tagManager.GetIptcKeywords(item.Name, cancellationToken))
                            merge.UnionWith(new[] { iptcTag });

                    if (!await _quickImageDatabase.AddTagsAsync(item.Name, merge, cancellationToken))
                    {
                        progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
                            new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, merge));
                }
                catch (Exception exception)
                {
                    progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
                }
            }
        }

        private async Task AddTags(IEnumerable<ListViewItem> items, IReadOnlyList<string> keywords, MagicMime magicMime,
            IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var mime = await magicMime.GetMimeType(item.Name, cancellationToken);

                    if (Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        if (!await _tagManager.AddIptcKeywords(item.Name, keywords, cancellationToken))
                        {
                            progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                            continue;
                        }
                    }

                    if (!await _quickImageDatabase.AddTagsAsync(item.Name, keywords, cancellationToken))
                    {
                        progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
                            new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, keywords, true));
                }
                catch (Exception exception)
                {
                    progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
                }
            }
        }

        private async Task StripTags(IEnumerable<ListViewItem> items, MagicMime magicMime,
            IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var mime = await magicMime.GetMimeType(item.Name, cancellationToken);

                    if (Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        if (!await _tagManager.StripIptcProfile(item.Name, cancellationToken))
                        {
                            progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
                                new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                            continue;
                        }
                    }

                    var tags = await _quickImageDatabase.GetTags(item.Name, cancellationToken).ToArrayAsync(cancellationToken);

                    if (tags.Length != 0)
                        if (!await _quickImageDatabase.StripTagsAsync(item.Name, cancellationToken))
                        {
                            progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
                                new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                            continue;
                        }

                    progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags, false));
                }
                catch (Exception exception)
                {
                    progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
                }
            }
        }

        private async Task RemoveTags(IEnumerable<ListViewItem> items, IReadOnlyList<string> keywords,
            MagicMime magicMime, IProgress<ImageListViewItemProgress<ListViewItem>> progress,
            CancellationToken cancellationToken)
        {
            foreach (var item in items)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                try
                {
                    var mime = await magicMime.GetMimeType(item.Name, cancellationToken);

                    if (Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        if (!await _tagManager.RemoveIptcKeywords(item.Name, keywords, cancellationToken))
                        {
                            progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
                                new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                            continue;
                        }
                    }

                    if (!await _quickImageDatabase.RemoveTagsAsync(item.Name, keywords, cancellationToken))
                    {
                        progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
                            new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
                        continue;
                    }

                    progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, keywords, false));
                }
                catch (Exception exception)
                {
                    progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
                }
            }
        }


        private void collapseToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            var listViewItems = items as ListViewItem[] ?? items.ToArray();

            foreach(var item in listViewItems)
            {
                var group = item.Group;

                if (!imageListView.GetCollapsed(group))
                {
                    imageListView.SetCollapsed(group, true);
                }
            }
        }

        private void collapseAllToolStripMenuItem_Click(object sender, EventArgs e)
        {
            foreach (var group in _imageListViewGroupDictionary.Values)
            {
                if (!imageListView.GetCollapsed(group))
                {
                    imageListView.SetCollapsed(group, true);
                }
            }
        }

        private void expandAllToolStripMenuItem_Click(object sender, EventArgs e)
        {
            foreach (var group in _imageListViewGroupDictionary.Values)
            {
                if (imageListView.GetCollapsed(group))
                {
                    imageListView.SetCollapsed(group, false);
                }
            }
        }

        private void Form1_Closing(object sender, FormClosingEventArgs e)
        {
        }

        private async void creationTimeAscendingSortMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(
                new DateImageListViewSorter(SortOrder.Ascending, DateImageListViewSorterType.Creation));
        }

        private async void creationTimeDescendingSortMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(
                new DateImageListViewSorter(SortOrder.Descending, DateImageListViewSorterType.Creation));
        }

        private async void accessTimeAscendingSortMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(
                new DateImageListViewSorter(SortOrder.Ascending, DateImageListViewSorterType.Access));
        }

        private async void accessTimeDescendingSortMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new DateImageListViewSorter(SortOrder.Descending,
                DateImageListViewSorterType.Access));
        }

        private async void modificationTimeAscendingSortMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new DateImageListViewSorter(SortOrder.Ascending,
                DateImageListViewSorterType.Modification));
        }

        private async void modificationTimeDescendingSortMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new DateImageListViewSorter(SortOrder.Descending,
                DateImageListViewSorterType.Modification));
        }

        private void renameToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_renameForm != null)
            {
                return;
            }

            var item = imageListView.SelectedItems.OfType<ListViewItem>().FirstOrDefault();

            if (item == null)
            {
                return;
            }

            _renameForm = new RenameForm(item);
            _renameForm.Rename += RenameForm_Rename;
            _renameForm.Closing += RenameForm_Closing;
            _renameForm.Show();
        }

        private async void RenameForm_Rename(object sender, RenameForm.RenameEventArgs e)
        {
            await RenameImageAsync(e.ListViewItem, e.FileName, _cancellationToken);
        }

        private void RenameForm_Closing(object sender, CancelEventArgs e)
        {
            if (_renameForm == null)
            {
                return;
            }

            _renameForm.Closing -= RenameForm_Closing;
            _renameForm.Dispose();
            _renameForm = null;
        }

        private async void relocateToToolStropMenuItem_Click(object sender, EventArgs e)
        {
            var dialog = new CommonOpenFileDialog
            {
                AddToMostRecentlyUsedList = true,
                Multiselect = false,
                IsFolderPicker = true
            };

            var groupItems = new List<ListViewItem>();

            if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
                {
                    foreach (var groupItem in item.Group.Items.OfType<ListViewItem>())
                    {
                        groupItems.Add(groupItem);
                    }
                }
            }

            RelocateTo(groupItems, dialog.FileName, _cancellationToken);
        }

        private void imageListView_MouseDown(object sender, MouseEventArgs e)
        {
        }

        private void checkBox2_CheckedChanged(object sender, EventArgs e)
        {
            var checkBox = (CheckBox)sender;
            switch (checkBox.Checked)
            {
                case true:
                    _quickImageSearchParameters =
                        _quickImageSearchParameters | QuickImageSearchParameters.Metadata;

                    break;
                case false:
                    _quickImageSearchParameters =
                        _quickImageSearchParameters & ~QuickImageSearchParameters.Metadata;
                    break;
            }

            var text = textBox1.Text;
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            if (_searchCancellationTokenSource != null)
            {
                _searchCancellationTokenSource.Cancel();
            }

            _searchCancellationTokenSource = new CancellationTokenSource();
            _searchCancellationToken = _searchCancellationTokenSource.Token;
            _linkedSearchCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
            _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;

            _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
        }

        private void checkBox3_CheckedChanged(object sender, EventArgs e)
        {
            var checkBox = (CheckBox)sender;
            switch (checkBox.Checked)
            {
                case true:
                    _quickImageSearchParameters =
                        _quickImageSearchParameters | QuickImageSearchParameters.Split;

                    break;
                case false:
                    _quickImageSearchParameters =
                        _quickImageSearchParameters & ~QuickImageSearchParameters.Split;
                    break;
            }

            var text = textBox1.Text;
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            if (_searchCancellationTokenSource != null)
            {
                _searchCancellationTokenSource.Cancel();
            }

            _searchCancellationTokenSource = new CancellationTokenSource();
            _searchCancellationToken = _searchCancellationTokenSource.Token;
            _linkedSearchCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
            _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;

            _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
        }

        private void checkBox2_VisibleChanged(object sender, EventArgs e)
        {
            var checkBox = (CheckBox)sender;
            if (checkBox.Checked)
            {
                _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.Metadata;

                return;
            }

            _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.Metadata;
        }

        private void checkBox3_VisibleChanged(object sender, EventArgs e)
        {
            var checkBox = (CheckBox)sender;
            if (checkBox.Checked)
            {
                _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.Split;

                return;
            }

            _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.Split;
        }

        private async void removeMissingToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            var listViewItems = items as ListViewItem[] ?? items.ToArray();

            await RemoveMissingAsync(listViewItems.Select(item => item.Group.Name), _cancellationToken);
        }

        private async Task PerformUpgrade()
        {
            // Manually check for updates, this will not show a ui
            var updateCheck = await _sparkle.CheckForUpdatesQuietly();

            switch (updateCheck.Status)
            {
                case UpdateStatus.UserSkipped:
                    var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;

                    updateCheck.Updates.Sort(UpdateComparer);
                    var latestVersion = updateCheck.Updates.FirstOrDefault();
                    if (latestVersion != null)
                    {
                        var availableVersion = new Version(latestVersion.Version);

                        if (availableVersion <= assemblyVersion)
                        {
                            return;
                        }
                    }

                    var decision = MessageBox.Show(
                        "Update available but it has been previously skipped. Should the update proceed anyway?",
                        Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.YesNo,
                        MessageBoxIcon.Asterisk,
                        MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);

                    if (decision.HasFlag(DialogResult.No))
                    {
                        return;
                    }

                    goto default;
                case UpdateStatus.UpdateNotAvailable:
                    MessageBox.Show("No updates available at this time.",
                        Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.OK,
                        MessageBoxIcon.Asterisk,
                        MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);
                    break;
                case UpdateStatus.CouldNotDetermine:
                    Log.Error("Could not determine the update availability status.");
                    break;
                default:
                    _sparkle.ShowUpdateNeededUI();
                    break;
            }
        }

        private static int UpdateComparer(AppCastItem x, AppCastItem y)
        {
            if (x == null) return 1;

            if (y == null) return -1;

            if (x == y) return 0;

            return new Version(y.Version).CompareTo(new Version(x.Version));
        }

        private async void oCRTextToTagsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            var listViewItems = items as ListViewItem[] ?? items.ToArray();

            if (listViewItems.Length == 0)
            {
                return;
            }

            await OcrImagesAsync(listViewItems, _cancellationToken);
        }

        #region Static Methods

        private static async IAsyncEnumerable<(string File, Stream Data, Definition Mime)> GetDragDropFiles(
            IDataObject data, MagicMime magicMime, [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            var files = (string[])data.GetData(DataFormats.FileDrop);
            if (files != null)
            {
                foreach (var file in files)
                {
                    var fileAttributes = File.GetAttributes(file);
                    if (fileAttributes.HasFlag(FileAttributes.Directory))
                    {
                        continue;
                    }

                    using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
                    var memoryStream = new MemoryStream();
                    await fileStream.CopyToAsync(memoryStream);
                    memoryStream.Position = 0L;

                    var mime = await magicMime.Identify(file, cancellationToken);
                    if (mime == null)
                    {
                        continue;
                    }

                    yield return (File: file, Data: memoryStream, Mime: mime.Definition);
                }

                yield break;
            }

            var fileNames = data.GetFileContentNames();
            for (var i = 0; i < fileNames.Length; ++i)
            {
                var memoryStream = data.GetFileContent(i);
                memoryStream.Position = 0L;

                MagicMimeFile mime;
                try
                {
                    mime = magicMime.Identify(fileNames[i], memoryStream, cancellationToken);

                    if (mime == null)
                    {
                        continue;
                    }
                }
                catch(Exception exception)
                {
                    Log.Error(exception, $"Could not determine mime type for file {fileNames[i]}.");

                    continue;
                }

                yield return (File: fileNames[0], Data: memoryStream, Mime: mime.Definition);
            }
        }

        private static async IAsyncEnumerable<string> GetFilesAsync(string entry,
            Configuration.Configuration configuration, MagicMime magicMime,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions { CancellationToken = cancellationToken });

#pragma warning disable CS4014
            Task.Run(async () =>
#pragma warning restore CS4014
            {
                try
                {
                    var attributes = File.GetAttributes(entry);
                    if (attributes.HasFlag(FileAttributes.Directory))
                    {
                        var directoryFiles = Directory.GetFiles(entry);
                        foreach (var directoryFile in directoryFiles)
                        {
                            if (!File.Exists(directoryFile))
                            {
                                continue;
                            }

                            var fileMimeType = await magicMime.GetMimeType(directoryFile, cancellationToken);

                            if (!configuration.SupportedFormats.IsSupported(fileMimeType))
                            {
                                continue;
                            }

                            await bufferBlock.SendAsync(directoryFile, cancellationToken);
                        }

                        return;
                    }

                    var entryMimeType = await magicMime.GetMimeType(entry, cancellationToken);
                    if (!configuration.SupportedFormats.IsSupported(entryMimeType))
                    {
                        return;
                    }

                    await bufferBlock.SendAsync(entry, cancellationToken);
                }
                finally
                {
                    bufferBlock.Complete();
                }
            }, cancellationToken);

            while (await bufferBlock.OutputAvailableAsync(cancellationToken))
            {
                if (!bufferBlock.TryReceiveAll(out var files))
                {
                    continue;
                }

                foreach (var file in files)
                {
                    yield return file;
                }
            }
        }

        public static async Task SaveConfiguration(Configuration.Configuration configuration)
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
                Directory.CreateDirectory(Constants.UserApplicationDirectory);

            switch (await Serialization.Serialize(configuration, Constants.ConfigurationFile, "Configuration",
                        "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
                        CancellationToken.None))
            {
                case SerializationSuccess<Configuration.Configuration> _:
                    Log.Information("Configuration serialized successfully");
                    break;
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception.Message, "Configuration failed to serialize");
                    break;
            }
        }

        public static async Task<Configuration.Configuration> LoadConfiguration()
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
                Directory.CreateDirectory(Constants.UserApplicationDirectory);

            var deserializationResult =
                await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
                    Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);

            switch (deserializationResult)
            {
                case SerializationSuccess<Configuration.Configuration> serializationSuccess:
                    return serializationSuccess.Result;
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception, "Configuration failed to deserialize");
                    return new Configuration.Configuration();
                default:
                    return new Configuration.Configuration();
            }
        }

        private static async Task<Bitmap> CreateThumbnail(Stream file, uint width, uint height,
            CancellationToken cancellationToken)
        {
            using var memoryStream = new MemoryStream();
            using var imageCollection =
                new MagickImageCollection(file, new MagickReadSettings { FrameIndex = 0, FrameCount = 1 });
            var frame = imageCollection[0];

            var scaleHeight = width / (float)frame.Height;
            var scaleWidth = height / (float)frame.Width;
            var scale = Math.Min(scaleHeight, scaleWidth);

            width = (uint)(frame.Width * scale);
            height = (uint)(frame.Height * scale);

            var geometry = new MagickGeometry(width, height);
            frame.Resize(geometry);

            await frame.WriteAsync(memoryStream, MagickFormat.Bmp, cancellationToken);

            using var image = new MagickImage(MagickColors.Transparent, 128, 128);
            memoryStream.Position = 0L;
            using var composite = await new MagickFactory().Image.CreateAsync(memoryStream, cancellationToken);
            image.Composite(composite, Gravity.Center, CompositeOperator.Over);

            using var outputStream = new MemoryStream();
            await image.WriteAsync(outputStream, MagickFormat.Bmp, cancellationToken);
            var optimizer = new ImageOptimizer { IgnoreUnsupportedFormats = true };
            outputStream.Position = 0L;
            optimizer.Compress(outputStream);

            outputStream.Position = 0L;
            return (Bitmap)Image.FromStream(outputStream, true);
        }

        private static int[] CreateHistogram(MemoryStream bitmapMemoryStream, CancellationToken cancellationToken,
            int threads = 2)
        {
            using var bitmap = (Bitmap)Image.FromStream(bitmapMemoryStream);

            var histogram = new int[0xFFFFFFFF];
            histogram.Initialize();

            var parallelOptions = new ParallelOptions
            { CancellationToken = cancellationToken, MaxDegreeOfParallelism = threads / 2 };
            Parallel.For(0, bitmap.Width, parallelOptions, (x, state) =>
            {
                Parallel.For(0, bitmap.Height, parallelOptions, (y, state) =>
                {
                    var value = bitmap.GetPixel(x, y).ToArgb();

                    histogram[value]++;
                });
            });

            return histogram;
        }

        #endregion

        #region Event Handlers

        private void toolStripTextBox1_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.KeyCode != Keys.Return)
            {
                return;
            }

            // Skip the beep.
            e.Handled = true;

            var toolStripTextBox = (ToolStripTextBox)sender;
            var tagText = toolStripTextBox.Text;
            toolStripTextBox.Clear();

            if (string.IsNullOrEmpty(tagText))
            {
                return;
            }

            var keywords = new[] { tagText };

            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            var count = items.Length;

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;
            toolStripProgressBar1.Value = 0;

            void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
                        foreach (var tag in imageListViewItemProgressSuccess.Tags)
                        {
                            if (!_tagAutoCompleteStringCollection.Contains(tag))
                                _tagAutoCompleteStringCollection.Add(tag);

                            tagListView.BeginUpdate();
                            if (tagListView.Items.ContainsKey(tag))
                            {
                                tagListView.Items[tag].Checked = true;
                                tagListView.EndUpdate();

                                _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(1000), () =>
                                {
                                    tagListView.BeginUpdate();
                                    tagListView.Sort();
                                    tagListView.EndUpdate();
                                }, _formTaskScheduler, _cancellationToken);
                                continue;
                            }

                            tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                            tagListView.Items[tag].Checked = true;
                            tagListView.EndUpdate();

                            _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(1000), () =>
                            {
                                tagListView.BeginUpdate();
                                tagListView.Sort();
                                tagListView.EndUpdate();
                            }, _formTaskScheduler, _cancellationToken);
                        }
                        break;
                    case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
                        break;
                }

                toolStripStatusLabel1.Text = "Adding tags...";
                toolStripProgressBar1.Increment(1);

                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
                }
            }

            toolStripStatusLabel1.Text = "Adding tags...";
#pragma warning disable CS4014
            Task.Factory.StartNew(async () =>
#pragma warning restore CS4014
            {
                _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
                try
                {
                    await AddTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
                }
                catch
                {
                    _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
                }
            }, _cancellationToken);
        }

        private void contextMenuStrip1_Opened(object sender, EventArgs e)
        {
            tagTextBox.Focus();

        }

        private async void Form1_Load(object sender, EventArgs e)
        {
            JotFormTracker.Tracker.Track(this);

            JotFormTracker.Tracker.Configure<Form1>()
                .Properties(form => new { form.tabControl1.SelectedIndex });

            JotFormTracker.Tracker.Configure<Form1>()
                .Properties(form => new { form.splitContainer3.SplitterDistance });

            JotFormTracker.Tracker.Configure<Form1>()
                .Properties(form => new { form.radioButton1.Checked });

            JotFormTracker.Tracker.Configure<Form1>()
                .Properties(form => new { form.checkBox1.Checked, form.checkBox1.CheckState });

            JotFormTracker.Tracker.Configure<Form1>()
                .Properties(form => new { form.checkBox2.Checked, form.checkBox2.CheckState });

            JotFormTracker.Tracker.Configure<Form1>()
                .Properties(form => new { form.checkBox3.Checked, form.checkBox3.CheckState });


            _tagListViewSorter = new TagListViewSorter();
            tagListView.ListViewItemSorter = _tagListViewSorter;

            _tagAutoCompleteStringCollection = new AutoCompleteStringCollection();
            textBox1.AutoCompleteCustomSource = _tagAutoCompleteStringCollection;
            tagTextBox.AutoCompleteCustomSource = _tagAutoCompleteStringCollection;

            Configuration = await LoadConfiguration();

#pragma warning disable CS4014
            PerformUpgrade();
#pragma warning restore CS4014

#pragma warning disable CS4014
            Task.Factory.StartNew(async () =>
#pragma warning restore CS4014
            {
                try
                {
                    await _imageListViewLock.WaitAsync(_cancellationToken);
                }
                catch
                {
                    return;
                }

                this.InvokeIfRequired(form =>
                {
                    form.toolStripStatusLabel1.Text = "Loading images...";
                    form.toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
                    form.toolStripProgressBar1.MarqueeAnimationSpeed = 30;
                });

                try
                {
                    var imageListViewItems = new ConcurrentBag<ListViewItem>();
                    var tags = new HashSet<string>(StringComparer.Ordinal);
                    await foreach (var image in _quickImageDatabase.GetAll(_cancellationToken))
                    {
                        if (!largeImageList.Images.ContainsKey(image.File))
                        {
                            largeImageList.Images.Add(image.File, image.Thumbnail);
                        }

                        var fileInfo = new FileInfo(image.File);

                        if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
                        {
                            group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left) { Name = fileInfo.DirectoryName };

                            _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);

                            imageListView.InvokeIfRequired(view =>
                            {
                                view.BeginUpdate();
                                view.Groups.Add(group);
                                view.EndUpdate();
                            });
                        }

                        tags.UnionWith(image.Tags);

                        var imageListViewItem = new ListViewItem(image.File)
                        {
                            Name = image.File,
                            ImageKey = image.File,
                            Text = fileInfo.Name,
                            Group = group
                        };

                        imageListViewItems.Add(imageListViewItem);

                    }

                    this.InvokeIfRequired(_ => { _tagAutoCompleteStringCollection.AddRange(tags.ToArray()); });

                    imageListView.InvokeIfRequired(view =>
                    {
                        view.BeginUpdate();
                        view.Items.AddRange(imageListViewItems.ToArray());
                        view.EndUpdate();
                    });

                    tagListView.InvokeIfRequired(view =>
                    {
                        view.BeginUpdate();
                        view.Items.AddRange(tags.Select(tag => new ListViewItem(tag) { Name = tag }).ToArray());
                        view.EndUpdate();
                    });
                }
                catch (Exception exception)
                {
                    Log.Error(exception, "Unable to load images.");
                }
                finally
                {
                    this.InvokeIfRequired(form =>
                    {
                        form.toolStripStatusLabel1.Text = "Done loading images.";
                        form.toolStripProgressBar1.MarqueeAnimationSpeed = 0;
                    });

                    _imageListViewLock.Release();
                }
            }, _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        private async void tagListView_MouseDown(object sender, MouseEventArgs e)
        {
            var listView = (ListView)sender;
            if (!listView.CheckBoxes)
            {
                return;
            }

            // Allow clicking anywhere on tag.
            var hitTest = listView.HitTest(e.Location);
            if (hitTest.Item == null)
            {
                return;
            }

            var item = hitTest.Item;

            var tagText = item.Text;

            var keywords = new[] { tagText };

            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = items.Length;
            toolStripProgressBar1.Value = 0;

            void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
                        foreach (var tag in imageListViewItemProgressSuccess.Tags)
                        {
                            tagListView.BeginUpdate();
                            if (tagListView.Items.ContainsKey(tag))
                            {
                                tagListView.Items[tag].Checked = imageListViewItemProgressSuccess.Check;
                                tagListView.EndUpdate();
                                continue;
                            }

                            tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                            tagListView.Items[tag].Checked = imageListViewItemProgressSuccess.Check;
                            tagListView.EndUpdate();
                        }
                        break;
                    case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
                        break;
                }

                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
                }
            }

            if (item.Checked)
            {
                
                toolStripStatusLabel1.Text = "Removing tags...";

                _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
                try
                {
                    await RemoveTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
                }
                finally
                {
                    _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
                }

                switch(hitTest.Location)
                {
                    case ListViewHitTestLocations.Label:
                        hitTest.Item.Checked = !hitTest.Item.Checked;
                        break;
                }

                return;
            }

            toolStripStatusLabel1.Text = "Adding tags...";
            _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
            try
            {
                await AddTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
            }
            finally
            {
                _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
            }

            switch(hitTest.Location)
            {
                case ListViewHitTestLocations.Label:
                    hitTest.Item.Checked = !hitTest.Item.Checked;
                    break;
            }
        }

        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_aboutForm != null)
            {
                return;
            }

            _aboutForm = new AboutForm();
            _aboutForm.Closing += AboutForm_Closing;
            _aboutForm.Show();
        }

        private void AboutForm_Closing(object sender, CancelEventArgs e)
        {
            if (_aboutForm == null)
            {
                return;
            }

            _aboutForm.Closing -= AboutForm_Closing;
            _aboutForm.Dispose();
            _aboutForm = null;
        }

        private async void updateToolStripMenuItem_Click(object sender, EventArgs e)
        {
            await PerformUpgrade();
        }

        private void viewLogsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_viewLogsForm != null)
            {
                return;
            }

            _viewLogsForm = new ViewLogsForm(this, _memorySink, _cancellationToken);
            _viewLogsForm.Closing += ViewLogsForm_Closing;
            _viewLogsForm.Show();
        }

        private void ViewLogsForm_Closing(object sender, CancelEventArgs e)
        {
            if (_viewLogsForm == null)
            {
                return;
            }

            _viewLogsForm.Closing -= ViewLogsForm_Closing;
            _viewLogsForm.Close();
            _viewLogsForm = null;
        }

        private void quitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _cancellationTokenSource.Cancel();
            Close();
        }

        private async void removeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            RemoveImages(items, _cancellationToken);
        }

        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            var textBox = (TextBox)sender;
            var text = textBox.Text;

            if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();

            _searchCancellationTokenSource = new CancellationTokenSource();
            _searchCancellationToken = _searchCancellationTokenSource.Token;
            _linkedSearchCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
            _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;

            if (string.IsNullOrEmpty(text))
            {
                _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250),
                    async () => { await EndSearch(); }, _combinedSearchSelectionCancellationToken);

                return;
            }

            _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
                async text => { await BeginSearch(text); }, _combinedSearchSelectionCancellationToken);
        }

        private void imageListView_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Control && e.KeyCode == Keys.A)
            {
                foreach (var item in imageListView.Items.OfType<ListViewItem>())
                {
                    item.Selected = true;
                }
            }
        }

        private void imageListView_DragLeave(object sender, EventArgs e)
        {
        }

        private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            DeleteImages(items, _cancellationToken);
        }

        private void selectAllToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
            {
                foreach (var groupItem in item.Group.Items.OfType<ListViewItem>())
                {
                    groupItem.Selected = true;
                }
            }
        }

        private void selectAllToolStripMenuItem_Click(object sender, EventArgs e)
        {
            foreach (var item in imageListView.Items.OfType<ListViewItem>())
            {
                item.Selected = true;
            }
        }

        private void textBox1_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Control && e.KeyCode == Keys.A)
            {
                var textBox = (TextBox)sender;

                textBox.SelectAll();
            }
        }

        private void radioButton2_CheckedChanged(object sender, EventArgs e)
        {
            _quickImageSearchType = QuickImageSearchType.Any;

            var text = textBox1.Text;
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            if (_searchCancellationTokenSource != null)
            {
                _searchCancellationTokenSource.Cancel();
            }

            _searchCancellationTokenSource = new CancellationTokenSource();
            _searchCancellationToken = _searchCancellationTokenSource.Token;
            _linkedSearchCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
            _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;

            _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
        }

        private void radiobutton1_CheckedChanged(object sender, EventArgs e)
        {
            _quickImageSearchType = QuickImageSearchType.All;

            var text = textBox1.Text;
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();

            _searchCancellationTokenSource = new CancellationTokenSource();
            _searchCancellationToken = _searchCancellationTokenSource.Token;
            _linkedSearchCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
            _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;

            _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
        }

        private async void editToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            var item = imageListView.SelectedItems.OfType<ListViewItem>().FirstOrDefault();
            if (item == null)
            {
                return;
            }

            if (_editorForm != null)
            {
                return;
            }

            try
            {
                var mime = await _magicMime.GetMimeType(item.Name, _cancellationToken);
                switch (mime)
                {
                    case "IMAGE/JPEG":
                    case "IMAGE/PNG":
                    case "IMAGE/BMP":
                        break;
                    default:
                        this.InvokeIfRequired(form =>
                        {
                            form.toolStripStatusLabel1.Text = $"Image format not supported for file {item.Name}";
                        });
                        return;
                }
            }
            catch(Exception exception)
            {
                Log.Error(exception, $"Could not deterine image format for file {item.Name}");
                return;
            }

            _editorForm = new EditorForm(item.Name, Configuration, _magicMime, _cancellationToken);
            _editorForm.ImageSave += _editorForm_ImageSave;
            _editorForm.ImageSaveAs += _editorForm_ImageSaveAs;
            _editorForm.Closing += _editorForm_Closing;
            _editorForm.Show();
        }

        private async void _editorForm_ImageSaveAs(object sender, EditorForm.ImageChangedEventArgs e)
        {
            using var imageMemoryStream = new MemoryStream(e.ImageBytes);
            using (var fileStream = new FileStream(e.FileName, FileMode.OpenOrCreate, FileAccess.Write))
            {
                await imageMemoryStream.CopyToAsync(fileStream);
            }

            await LoadFilesAsync(new[] { e.FileName }, _magicMime, _cancellationToken);
        }

        private async void _editorForm_ImageSave(object sender, EditorForm.ImageChangedEventArgs e)
        {
            using var bitmapMemoryStream = new MemoryStream();
            using var imageMemoryStream = new MemoryStream(e.ImageBytes);
            using (var fileStream = new FileStream(e.FileName, FileMode.OpenOrCreate, FileAccess.Write))
            {
                await imageMemoryStream.CopyToAsync(fileStream);
            }

            imageMemoryStream.Position = 0L;
            using var image = Image.FromStream(imageMemoryStream);
            image.Save(bitmapMemoryStream, ImageFormat.Bmp);

            bitmapMemoryStream.Position = 0L;
            using var hashBitmap = Image.FromStream(bitmapMemoryStream);
            var hash = ImagePhash.ComputeDigest(hashBitmap.ToBitmap().ToLuminanceImage());

            bitmapMemoryStream.Position = 0L;
            using var thumbnailBitmap = await CreateThumbnail(bitmapMemoryStream, 128, 128, _cancellationToken);
            var thumbnail = new Bitmap(thumbnailBitmap);
            thumbnailBitmap.Dispose();

            try
            {
                if (!await _quickImageDatabase.RemoveImageAsync(e.FileName, _cancellationToken))
                {
                    throw new ArgumentException($"Could not remove image {e.FileName} from database.");
                }

                var keywords = await _quickImageDatabase.GetTags(e.FileName, _cancellationToken).ToArrayAsync(_cancellationToken);

                if (!await _quickImageDatabase.AddImageAsync(e.FileName, hash, keywords, thumbnail, _cancellationToken))
                {
                    throw new ArgumentException($"Could not add image {e.FileName} to database.");
                }

                this.InvokeIfRequired(form =>
                {
                    form.largeImageList.Images.RemoveByKey(e.FileName);
                    form.largeImageList.Images.Add(e.FileName, thumbnail);
                });
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Could not update image in database");
            }
        }

        private void _editorForm_Closing(object sender, CancelEventArgs e)
        {
            if (_editorForm == null)
            {
                return;
            }

            _editorForm.ImageSave -= _editorForm_ImageSave;
            _editorForm.ImageSaveAs -= _editorForm_ImageSaveAs;
            _editorForm.Closing -= _editorForm_Closing;
            _editorForm.Dispose();
            _editorForm = null;
        }

        private async void synchronizeTagsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = items.Length;
            toolStripProgressBar1.Value = 0;

            void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
                        if (!(imageListViewItemProgressSuccess.Item is { } listViewItem))
                        {
                            break;
                        }

                        toolStripStatusLabel1.Text = $"Synchronizing tags for {listViewItem.Name}";

                        break;
                    case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
                        break;
                }

                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                {
                    _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
                    _imageListViewLock.Release();
                }
            }

            toolStripStatusLabel1.Text = "Synchronizing image tags with database tags...";
            try
            {
                await _imageListViewLock.WaitAsync(_cancellationToken);
            }
            catch
            {
                return;
            }

            _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
            try
            {
                await BalanceImageTags(items, _magicMime, _listViewItemProgress, _cancellationToken);
            }
            catch
            {
                _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
                _imageListViewLock.Release();
            }
        }

        private void checkBox1_VisibleChanged(object sender, EventArgs e)
        {
            var checkBox = (CheckBox)sender;
            if (checkBox.Checked)
            {
                _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.CaseSensitive;

                return;
            }

            _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.CaseSensitive;
        }

        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            var checkBox = (CheckBox)sender;
            switch (checkBox.Checked)
            {
                case true:
                    _quickImageSearchParameters =
                        _quickImageSearchParameters | QuickImageSearchParameters.CaseSensitive;

                    break;
                case false:
                    _quickImageSearchParameters =
                        _quickImageSearchParameters & ~QuickImageSearchParameters.CaseSensitive;
                    break;
            }

            var text = textBox1.Text;
            if (string.IsNullOrEmpty(text))
            {
                return;
            }

            if (_searchCancellationTokenSource != null)
            {
                _searchCancellationTokenSource.Cancel();
            }

            _searchCancellationTokenSource = new CancellationTokenSource();
            _searchCancellationToken = _searchCancellationTokenSource.Token;
            _linkedSearchCancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
            _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;

            _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
        }

        private void PreviewFormClosing(object sender, CancelEventArgs e)
        {
            if (_previewForm == null)
            {
                return;
            }

            _previewForm.Closing -= PreviewFormClosing;
            _previewForm.Dispose();
            _previewForm = null;
        }

        private async void perceptionToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            await SortImageListView(new PerceptionImageListViewItemSorter(
                imageListView.Items.OfType<ListViewItem>().ToList(),
                _quickImageDatabase,
                _cancellationToken));
        }

        private void openDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            var item = items.FirstOrDefault();
            if (item == null)
            {
                return;
            }

            Process.Start("explorer.exe", $"/select, \"{item.Name}\"");
        }

        private void aboutToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            if (_aboutForm != null)
            {
                return;
            }

            _aboutForm = new AboutForm();
            _aboutForm.Closing += AboutForm_Closing;
            _aboutForm.Show();
        }

        private async void imageListView_DragDrop(object sender, DragEventArgs e)
        {
            await LoadDataObjectAsync(e.Data);
        }

        private void imageListView_DragEnter(object sender, DragEventArgs e)
        {
            e.Effect = e.AllowedEffect;
        }

        private void imageListView_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            var listView = (ListView)sender;
            var info = listView.HitTest(e.X, e.Y);

            switch (info.Location)
            {
                case ListViewHitTestLocations.AboveClientArea:
                case ListViewHitTestLocations.BelowClientArea:
                case ListViewHitTestLocations.LeftOfClientArea:
                case ListViewHitTestLocations.RightOfClientArea:
                case ListViewHitTestLocations.None:
                    return;
            }

            var item = info.Item;

            if (item == null)
            {
                return;
            }

            try
            {
                if (_previewForm != null)
                {
                    _previewForm.InvokeIfRequired(form =>
                    {
                        form.Close();
                    });

                    _previewForm = null;
                }
            }
            catch(Exception exception)
            {
                Log.Warning(exception, "Error encoutnered while trying to close preview form.");
            }

            try
            {
                new Thread(() =>
                {
                    _previewForm = new PreviewForm(item.Name, Configuration, _magicMime, _cancellationToken);
                    _previewForm.Closing += PreviewFormClosing;
                    _previewForm.ShowDialog();
                }).Start();
            }
            catch (Exception exception)
            {
                Log.Error(exception, $"File {item.Name} could not be displayed due to the path not being accessible.");
            }
        }

        private void imageListView_MouseUp(object sender, MouseEventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            SelectTags(items);
        }

        private void imageListView_KeyUp(object sender, KeyEventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
            if (e.KeyCode == Keys.Delete)
            {
                RemoveImages(items, _cancellationToken);
                return;
            }

            SelectTags(items);
        }

        private async void imageListView_ItemDrag(object sender, ItemDragEventArgs e)
        {
            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Value = 0;
            toolStripProgressBar1.Maximum = 3;

            var inputBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions { CancellationToken = _cancellationToken });
            var fileTransformBlock =
                new TransformBlock<ListViewItem, (string Source, string Path, string Name, string Mime, string Extension)>(async item =>
                {
                    string mimeType = string.Empty;
                    try
                    {
                        mimeType = await _magicMime.GetMimeType(item.Name, _cancellationToken);
                    }
                    catch (Exception exception)
                    {
                        Log.Error(exception, $"Could not determine image type for file  {item.Name}");
                    }

                    var extension = Path.GetExtension(item.Name);
                    var path = Path.GetTempPath();
                    var file = Path.GetFileNameWithoutExtension(item.Name);
                    if (Configuration.OutboundDragDrop.RenameOnDragDrop)
                    {
                        switch (Configuration.OutboundDragDrop.DragDropRenameMethod)
                        {
                            case DragDropRenameMethod.Random:
                                file = string.Join("", Enumerable.Repeat(0, 5).Select(n => (char)_random.Next('a', 'z' + 1)));
                                break;
                            case DragDropRenameMethod.Timestamp:
                                file = $"{DateTimeOffset.Now.ToUnixTimeSeconds()}";
                                break;
                        }
                    }

                    this.InvokeIfRequired(form =>
                    {
                        form.toolStripStatusLabel1.Text = $"File {item.Name} scanned for drag and drop...";
                    });

                    return (Source: item.Name, Path: path, Name: file, Mime: mimeType, Extension: extension);


                }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

#pragma warning disable CS4014
            fileTransformBlock.Completion.ContinueWith(_ =>
#pragma warning restore CS4014
            {
                toolStripStatusLabel1.Text = "All files scanned for drag and drop.";
                toolStripProgressBar1.Increment(1);
            }, _formTaskScheduler);

            var noConvertTransformBlock =
                new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(async item =>
                    {
                        var destination = Path.Combine(item.Path, $"{item.Name}{item.Extension}");

                        await Miscellaneous.CopyFileAsync(item.Source, destination, _cancellationToken);

                        this.InvokeIfRequired(form =>
                        {
                            form.toolStripStatusLabel1.Text =
                                $"File {item.Source} does not need conversion for drag and drop...";
                        });
                        return destination;
                    }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

#pragma warning disable CS4014
            noConvertTransformBlock.Completion.ContinueWith(_ =>
#pragma warning restore CS4014
            {
                toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
                toolStripProgressBar1.Increment(1);
            }, _formTaskScheduler);

            var jpegTransformBlock =
                new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(async file =>
                    {
                        using var imageStream =
                            await _imageTool.ConvertTo(file.Source, MagickFormat.Jpeg, _cancellationToken);

                        var jpegDestination = Path.Combine(file.Path, $"{file.Name}.jpg");
                        await Miscellaneous.CopyFileAsync(imageStream, jpegDestination, _cancellationToken);

                        this.InvokeIfRequired(form =>
                        {
                            form.toolStripStatusLabel1.Text =
                                $"File {file.Source} converted to JPEG for drag and drop...";
                        });

                        return jpegDestination;
                    }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

#pragma warning disable CS4014
            jpegTransformBlock.Completion.ContinueWith(_ =>
#pragma warning restore CS4014
            {
                toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
                toolStripProgressBar1.Increment(1);
            }, _formTaskScheduler);

            var pngTransformBlock =
                new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(async file =>
                    {
                        using var imageStream = await _imageTool.ConvertTo(file.Source, MagickFormat.Png, _cancellationToken);

                        var pngDestination = Path.Combine(file.Path, $"{file.Name}.png");
                        await Miscellaneous.CopyFileAsync(imageStream, pngDestination, _cancellationToken);

                        this.InvokeIfRequired(form =>
                        {
                            form.toolStripStatusLabel1.Text =
                                $"File {file.Source} converted to PNG for drag and drop...";
                        });

                        return pngDestination;
                    }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

#pragma warning disable CS4014
            pngTransformBlock.Completion.ContinueWith(_ =>
#pragma warning restore CS4014
            {
                toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
                toolStripProgressBar1.Increment(1);
            }, _formTaskScheduler);

            var bmpTransformBlock =
                new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>( async file =>
                    {
                        using var imageStream = await _imageTool.ConvertTo(file.Source, MagickFormat.Jpeg, _cancellationToken);

                        var bmpDestination = Path.Combine(file.Path, $"{file.Name}.bmp");
                        await Miscellaneous.CopyFileAsync(imageStream, bmpDestination, _cancellationToken);

                        this.InvokeIfRequired(form =>
                        {
                            form.toolStripStatusLabel1.Text =
                                $"File {file.Source} converted to BMP for drag and drop...";
                        });

                        return bmpDestination;
                    }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

#pragma warning disable CS4014
            bmpTransformBlock.Completion.ContinueWith(_ =>
#pragma warning restore CS4014
            {
                toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
                toolStripProgressBar1.Increment(1);
            }, _formTaskScheduler);

            var iptcStripTransformBlock = new TransformBlock<string, string>(async file =>
            {
                try
                {
                    var mime = await _magicMime.GetMimeType(file, _cancellationToken);
                    if (Configuration.SupportedFormats.IsSupportedVideo(mime))
                    {
                        return file;
                    }

                    if (!Configuration.SupportedFormats.IsSupportedImage(mime))
                    {
                        throw new ArgumentException();
                    }

                    if (!await _tagManager.StripIptcProfile(file, _cancellationToken))
                    {
                        throw new ArgumentException();
                    }
                }
                catch
                {
                    this.InvokeIfRequired(form =>
                    {
                        form.toolStripStatusLabel1.Text =
                            $"Failed to strip IPTC tags for file {file} for drag and drop...";
                    });

                    return string.Empty;
                }

                this.InvokeIfRequired(form =>
                {
                    form.toolStripStatusLabel1.Text = $"Stripped IPTC tags from file {file} for drag and drop...";
                });

                return file;
            }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });

#pragma warning disable CS4014
            iptcStripTransformBlock.Completion.ContinueWith(_ =>
#pragma warning restore CS4014
            {
                toolStripStatusLabel1.Text = "All tags stripped for drag and drop.";
                toolStripProgressBar1.Increment(1);
            }, _formTaskScheduler);

            var outputBlock = new BufferBlock<string>(new DataflowBlockOptions
                { CancellationToken = _cancellationToken });

            using var _1 =
                inputBlock.LinkTo(fileTransformBlock, new DataflowLinkOptions { PropagateCompletion = true });

            using var _2 = fileTransformBlock.LinkTo(jpegTransformBlock, item =>
                Configuration.OutboundDragDrop.ConvertOnDragDrop &&
                !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
                !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
                Configuration.SupportedFormats.IsSupported(item.Mime) &&
                Configuration.OutboundDragDrop.DragDropConvertType == "image/jpeg" &&
                item.Mime != "image/jpeg");
            using var _3 =
                jpegTransformBlock.LinkTo(iptcStripTransformBlock,
                    file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
            using var _11 =
                jpegTransformBlock.LinkTo(outputBlock);
            using var _4 = fileTransformBlock.LinkTo(pngTransformBlock, item =>
                Configuration.OutboundDragDrop.ConvertOnDragDrop &&
                !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
                !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
                Configuration.SupportedFormats.IsSupported(item.Mime) &&
                Configuration.OutboundDragDrop.DragDropConvertType == "image/png" &&
                item.Mime != "image/png");
            using var _5 =
                pngTransformBlock.LinkTo(iptcStripTransformBlock,
                    file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
            using var _12 =
                pngTransformBlock.LinkTo(outputBlock);
            using var _6 = fileTransformBlock.LinkTo(bmpTransformBlock, item =>
                Configuration.OutboundDragDrop.ConvertOnDragDrop &&
                !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
                !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
                Configuration.SupportedFormats.IsSupported(item.Mime) &&
                Configuration.OutboundDragDrop.DragDropConvertType == "image/bmp" &&
                item.Mime != "image/bmp");
            using var _7 =
                bmpTransformBlock.LinkTo(iptcStripTransformBlock,
                    file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
            using var _13 =
                bmpTransformBlock.LinkTo(outputBlock);

            using var _8 = fileTransformBlock.LinkTo(noConvertTransformBlock);
            using var _15 = fileTransformBlock.LinkTo(DataflowBlock
                .NullTarget<(string Source, string Path, string Name, string Mime, string Extension)>());

            using var _9 = noConvertTransformBlock.LinkTo(iptcStripTransformBlock,
                file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);

            using var _10 =
                iptcStripTransformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
            using var _16 = iptcStripTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
            using var _14 =
                noConvertTransformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
            using var _17 = noConvertTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());

            foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
            {
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
                inputBlock.SendAsync(item, _cancellationToken);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
            }

            inputBlock.Complete();

            await fileTransformBlock.Completion.ContinueWith(_ =>
            {
                jpegTransformBlock.Complete();
                pngTransformBlock.Complete();
                bmpTransformBlock.Complete();
                noConvertTransformBlock.Complete();
                iptcStripTransformBlock.Complete();
            }, _cancellationToken);

            await Task.WhenAll(
                jpegTransformBlock.Completion, 
                pngTransformBlock.Completion,
                bmpTransformBlock.Completion,
                iptcStripTransformBlock.Completion,
                noConvertTransformBlock.Completion).ContinueWith(_ => 
                {
                    outputBlock.Complete();
                }, _cancellationToken);

            var files = new HashSet<string>();
            while (await outputBlock.OutputAvailableAsync(_cancellationToken))
            {
                if (!outputBlock.TryReceiveAll(out var items))
                {
                    continue;
                }

                files.UnionWith(items);
            }

            files.RemoveWhere(string.IsNullOrEmpty);

            this.InvokeIfRequired(form =>
            {
                form.toolStripStatusLabel1.Text = $"{files.Count} ready for drag and drop...";
            });

            if (files.Count == 0)
            {
                return;
            }

            var data = new DataObject(DataFormats.FileDrop, files.ToArray());
            this.InvokeIfRequired(_ => 
            { 
                DoDragDrop(data, DragDropEffects.Copy); 
            });
        }

        private void imageListView_DragOver(object sender, DragEventArgs e)
        {
        }

        private async void nameToolStripMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new NameImageListViewItemSorter());
        }

        private async void ascendingToolStripMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new SizeImageListViewItemSorter(SortOrder.Ascending));
        }

        private async void descendingToolStripMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new SizeImageListViewItemSorter(SortOrder.Descending));
        }

        private async void typeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            await SortImageListView(new TypeImageListViewItemSorter());
        }

        private async void importToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var dialog = new CommonOpenFileDialog
            {
                AddToMostRecentlyUsedList = true,
                Multiselect = true,
                IsFolderPicker = false
            };

            switch (dialog.ShowDialog())
            {
                case CommonFileDialogResult.Ok:
                    await LoadFilesAsync(dialog.FileNames, _magicMime, _cancellationToken);
                    break;
            }
        }

        private async void importDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var dialog = new CommonOpenFileDialog
            {
                AddToMostRecentlyUsedList = true,
                Multiselect = true,
                IsFolderPicker = true
            };

            switch (dialog.ShowDialog())
            {
                case CommonFileDialogResult.Ok:
                    await LoadFilesAsync(dialog.FileNames, _magicMime, _cancellationToken);
                    break;
            }
        }

        private void settingsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_settingsForm != null)
            {
                return;
            }

            _settingsForm = new SettingsForm(Configuration, _cancellationToken);
            _settingsForm.Closing += SettingsForm_Closing;
            _settingsForm.Show();
        }

        private void SettingsForm_Closing(object sender, CancelEventArgs e)
        {
            if (_settingsForm == null)
            {
                return;
            }

            if (_settingsForm.SaveOnClose)
            {
                // Commit the configuration.
                _changedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
                    async () => { await SaveConfiguration(Configuration); }, _cancellationToken);
            }

            _settingsForm.Closing -= SettingsForm_Closing;
            _settingsForm.Dispose();
            _settingsForm = null;
        }

        private async void removeTagsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            var count = items.Length;

            toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;
            toolStripProgressBar1.Value = 0;

            void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
            {
                switch (e)
                {
                    case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
                        if (!(imageListViewItemProgressSuccess.Item is { } imageListViewItem)) break;

                        foreach (var tag in imageListViewItemProgressSuccess.Tags)
                        {
                            tagListView.BeginUpdate();
                            if (tagListView.CheckedItems.ContainsKey(tag))
                            {
                                tagListView.Items[tag].Checked = false;
                                tagListView.EndUpdate();
                                continue;
                            }

                            tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
                            tagListView.Items[tag].Checked = false;
                            tagListView.EndUpdate();
                        }

                        break;
                    case ImageListViewItemProgressFailure<ListViewItem> _:
                        break;
                }

                toolStripStatusLabel1.Text = "Stripping tags...";
                toolStripProgressBar1.Increment(1);
                if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
                    _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
            }

            toolStripStatusLabel1.Text = "Stripping tags...";

            _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
            try
            {
                await StripTags(items, _magicMime, _listViewItemProgress, _cancellationToken);
            }
            catch
            {
                _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
            }
        }

        private async void balanceTagsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            var listViewItems = items as ListViewItem[] ?? items.ToArray();
            if (listViewItems.Length < 2)
            {
                return;
            }

            await BalanceTags(listViewItems);
        }

        private async void loadMissingToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            var listViewItems = items as ListViewItem[] ?? items.ToArray();

            await LoadFilesAsync(listViewItems.Select(item => item.Group.Name), _magicMime, _cancellationToken);
        }

        private void moveToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
        {
            var menuItem = (ToolStripMenuItem)sender;

            foreach (var group in _imageListViewGroupDictionary.Keys)
            {
                if (menuItem.DropDownItems.ContainsKey(group))
                {
                    continue;
                }

                var toolStripMenuSubItem = new ToolStripButton(group) { Name = group };
                toolStripMenuSubItem.Click += moveTargetToolStripMenuItem_Click;
                menuItem.DropDownItems.Add(toolStripMenuSubItem);
            }
        }

        private void moveToolStripMenuItem_DropDownClosed(object sender, EventArgs e)
        {
            var menuItem = (ToolStripMenuItem)sender;

            var items = new ConcurrentBag<ToolStripButton>();
            foreach(var toolStripMenuSubItem in menuItem.DropDownItems.OfType<ToolStripButton>())
            {
                toolStripMenuSubItem.Click -= moveTargetToolStripMenuItem_Click;

                items.Add(toolStripMenuSubItem);
            }

            foreach(var toolStripMenuSubItem in items)
            {
                menuItem.DropDownItems.Remove(toolStripMenuSubItem);
            }
        }

        private async void moveTargetToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var menuItem = (ToolStripButton)sender;

            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            await MoveImagesAsync(items, menuItem.Name, _cancellationToken);
        }

        private void imageListView_GroupHeaderClick(object sender, ListViewGroup e)
        {
            var listViewCollapsible = (ListViewCollapsible)sender;

            var collapsed = listViewCollapsible.GetCollapsed(e);
            if (collapsed)
            {
                listViewCollapsible.SetCollapsed(e, false);
                return;
            }

            listViewCollapsible.SetCollapsed(e, true);
        }

        private async void convertToTypeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var toolStripMenuItem = (ToolStripMenuItem)sender;
            var extension = toolStripMenuItem.Text;

            var extensionToMime = new Dictionary<string, string>
            {
                { "jpg", "image/jpeg" },
                { "png", "image/png" },
                { "bmp", "image/bmp" },
                { "gif", "image/gif" }
            };

            if (!Configuration.SupportedFormats.Images.Image.Contains(extensionToMime[extension]))
            {
                return;
            }

            var items = imageListView.SelectedItems.OfType<ListViewItem>();

            await ConvertImagesAsync(items, extension, _cancellationToken);
        }

        #endregion

        private void button1_Click(object sender, EventArgs e)
        {
            _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), () =>
            {
                tagListView.BeginUpdate();
                tagListView.Sort();
                tagListView.EndUpdate();
            }, _formTaskScheduler, _cancellationToken);
        }

        /// <summary>
        /// Enable or disable menu items recursively.
        /// </summary>
        /// <param name="item">the menu item from where to start disabling or enabling menu items</param>
        /// <param name="enable">whether to enable or disable the menu item
        /// </param>
        private void ToggleMenuItemsRecursive(ToolStripMenuItem item, MenuItemsToggleOperation menuItemsToggleOperation)
        {
            if (!item.HasDropDown)
            {
                return;
            }

            switch (menuItemsToggleOperation)
            {
                case MenuItemsToggleOperation.NONE:
                    throw new ArgumentException("Unknown menu toggle operation.");
                case MenuItemsToggleOperation.ENABLE:
                    item.Enabled = true;
                    break;
                case MenuItemsToggleOperation.DISABLE:
                    item.Enabled = false;
                    break;
            }

            foreach (var menuItem in item.DropDownItems.OfType<ToolStripMenuItem>())
            {
                ToggleMenuItemsRecursive(menuItem, menuItemsToggleOperation);
            }
        }

        private void contextMenuStrip1_Closing(object sender, ToolStripDropDownClosingEventArgs e)
        {
            ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
            ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.ENABLE);
            ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
            ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
        }

        private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
        {
            var mousePoint = imageListView.PointToClient(Cursor.Position);
            var listViewHitTestInfo = imageListView.HitTest(mousePoint);
            // check if the mouse was hovering over an item
            if (listViewHitTestInfo.Item != null)
            {
                // hovering
                ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
                ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.ENABLE);
                ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
                ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
                return;
            }

            // disable menu items not related to list view items
            ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
            ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.DISABLE);
            ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
            ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
        }

        private void copyCtrlToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
            switch(items.Length)
            {
                case 0:
                    break;
                default:
                    // copy file paths as the default for multiple files selected
                    var paths = new StringCollection();
                    foreach (var item in items)
                    {
                        paths.Add(item.Name);
                    }

                    Clipboard.SetFileDropList(paths);
                    break;
            }
        }

        private async void copyAsMediaToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();

            switch (items.Length)
            {
                case 1:

                    {
                        var file = items[0].Name;

                        using var memoryStream = new MemoryStream();
                        using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
                        await fileStream.CopyToAsync(memoryStream);
                        memoryStream.Position = 0L;

                        MagicMimeFile mime;
                        try
                        {
                            mime = _magicMime.Identify(file, memoryStream, _cancellationToken);

                            if (mime == null)
                            {
                                break;
                            }
                        }
                        catch (Exception exception)
                        {
                            Log.Error(exception, $"Could not determine mime type for file {file}.");

                            break;
                        }

                        if (Configuration.SupportedFormats.IsSupportedVideo(mime.Definition.File.MimeType))
                        {


                            Clipboard.SetDataObject(memoryStream);
                            break;
                        }

                        if (!Configuration.SupportedFormats.IsSupportedImage(mime.Definition.File.MimeType))
                        {
                            using var image = Image.FromFile(file);
                            Clipboard.SetImage(image);
                            break;
                        }
                    }

                    break;
                case 0:
                    break;
                default:
                    // copy file paths as the default for multiple files selected
                    var paths = new StringCollection();
                    foreach (var item in items)
                    {
                        paths.Add(item.Name);
                    }

                    Clipboard.SetFileDropList(paths);
                    break;
            }

        }

        private async void pasteCtrlCToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var dataObject = Clipboard.GetDataObject();

            await LoadDataObjectAsync(dataObject);
        }


    }
}

Generated by GNU Enscript 1.6.5.90.