Horizon – Rev 11

Subversion Repositories:
Rev:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.SQLite;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Horizon.Database;
using Horizon.Utilities;
using Microsoft.WindowsAPICodePack.Dialogs;
using Org.BouncyCastle.Crypto;
using Serilog;

namespace Horizon.Snapshots
{
    public partial class SnapshotManagerForm : Form
    {
        #region Static Fields and Constants

        private static ScheduledContinuation _searchTextBoxChangedContinuation;

        #endregion

        #region Public Events & Delegates

        public event EventHandler<PreviewRetrievedEventArgs> PreviewRetrieved;

        #endregion

        #region Private Delegates, Events, Enums, Properties, Indexers and Fields

        private readonly MainForm _mainForm;

        private readonly SnapshotDatabase _snapshotDatabase;

        private HexViewForm _hexViewForm;

        private SnapshotNoteForm _snapshotNote;

        private SnapshotPreviewForm _snapshotPreviewForm;

        private readonly object _mouseMoveLock = new object();

        private readonly CancellationTokenSource _cancellationTokenSource;

        private readonly CancellationToken _cancellationToken;

        private readonly CancellationTokenSource _localCancellationTokenSource;

        private readonly CancellationToken _localCancellationToken;

        #endregion

        #region Constructors, Destructors and Finalizers

        private SnapshotManagerForm()
        {
            InitializeComponent();

            dataGridView1.Columns["TimeColumn"].ValueType = typeof(DateTime);

            _searchTextBoxChangedContinuation = new ScheduledContinuation();

            _localCancellationTokenSource = new CancellationTokenSource();
            _localCancellationToken = _localCancellationTokenSource.Token;
        }

        public SnapshotManagerForm(MainForm mainForm, SnapshotDatabase snapshotDatabase,
                                   CancellationToken cancellationToken) : this()
        {
            _mainForm = mainForm;
            _snapshotDatabase = snapshotDatabase;
            _snapshotDatabase.SnapshotCreate += SnapshotManager_SnapshotCreate;

            _cancellationTokenSource =
                CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, cancellationToken);
            _cancellationToken = _cancellationTokenSource.Token;
        }

        /// <summary>
        ///     Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && components != null)
            {
                components.Dispose();
            }

            _snapshotDatabase.SnapshotCreate -= SnapshotManager_SnapshotCreate;

            _localCancellationTokenSource.Cancel();

            base.Dispose(disposing);
        }

        #endregion

        #region Event Handlers
        private void DataGridView1_MouseDown(object sender, MouseEventArgs e)
        {
            var dataGridView = (DataGridView)sender;

            var index = dataGridView.HitTest(e.X, e.Y).RowIndex;

            if (index == -1)
            {
                base.OnMouseDown(e);
                return;
            }

            if (!dataGridView.SelectedRows.Contains(dataGridView.Rows[index]))
            {
                base.OnMouseDown(e);
            }
        }

        private async void DataGridView1_MouseMove(object sender, MouseEventArgs e)
        {
            var dataGridView = (DataGridView)sender;

            // Only accept dragging with left mouse button.
            switch (e.Button)
            {
                case MouseButtons.Left:

                    if (!Monitor.TryEnter(_mouseMoveLock))
                    {
                        break;
                    }

                    try
                    {
                        var index = dataGridView.HitTest(e.X, e.Y).RowIndex;

                        if (index == -1)
                        {
                            base.OnMouseMove(e);
                            return;
                        }

                        var rows = GetSelectedDataGridViewRows(dataGridView);

                        var count = rows.Count;

                        if (count == 0)
                        {
                            base.OnMouseMove(e);
                            break;
                        }

                        toolStripProgressBar1.Minimum = 0;
                        toolStripProgressBar1.Maximum = count;

                        var virtualFileDataObject = new VirtualFileDataObject.VirtualFileDataObject();
                        var fileDescriptors =
                            new List<VirtualFileDataObject.VirtualFileDataObject.FileDescriptor>(count);

                        var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
                        {
                            if (_cancellationToken.IsCancellationRequested)
                            {
                                return;
                            }

                            if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                            {
                                Log.Error(rowProgressFailure.Exception, "Unable to retrieve data for row.");

                                toolStripStatusLabel1.Text =
                                    $"Could not read file data {rowProgress.Row.Cells["NameColumn"].Value}...";
                                toolStripProgressBar1.Value = rowProgress.Index + 1;

                                statusStrip1.Update();

                                return;
                            }

                            if (rowProgress is DataGridViewRowProgressSuccessRetrieveFileStream
                                rowProgressSuccessRetrieveFileStream)
                            {
                                toolStripStatusLabel1.Text =
                                    $"Got {rowProgress.Row.Cells["NameColumn"].Value} file stream...";
                                toolStripProgressBar1.Value = rowProgress.Index + 1;

                                statusStrip1.Update();

                                var hash = (string)rowProgressSuccessRetrieveFileStream.Row.Cells["HashColumn"].Value;
                                var name = (string)rowProgressSuccessRetrieveFileStream.Row.Cells["NameColumn"].Value;

                                var fileDescriptor = new VirtualFileDataObject.VirtualFileDataObject.FileDescriptor
                                {
                                    Name = name,
                                    StreamContents = stream =>
                                    {
                                        rowProgressSuccessRetrieveFileStream.MemoryStream.Seek(0, SeekOrigin.Begin);

                                        rowProgressSuccessRetrieveFileStream.MemoryStream.CopyTo(stream);
                                    }
                                };

                                fileDescriptors.Add(fileDescriptor);
                            }
                        });

                        await Task.Run(() => RetrieveFileStream(rows, progress, _cancellationToken), _cancellationToken);

                        if (_cancellationToken.IsCancellationRequested)
                        {
                            toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                            toolStripStatusLabel1.Text = "Done.";
                        }

                        virtualFileDataObject.SetData(fileDescriptors);

                        dataGridView1.DoDragDrop(virtualFileDataObject, DragDropEffects.Copy);
                    }
                    finally
                    {
                        Monitor.Exit(_mouseMoveLock);
                    }

                    break;
            }
        }

        private async Task RetrieveFileStream(IReadOnlyList<DataGridViewRow> rows,
                                             IProgress<DataGridViewRowProgress> progress,
                                             CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    var fileStream = await _snapshotDatabase.RetrieveFileStreamAsync(
                        (string)rows[i].Cells["HashColumn"].Value,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccessRetrieveFileStream(rows[i], i, fileStream));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private void SnapshotManagerForm_Resize(object sender, EventArgs e)
        {
            if (_snapshotPreviewForm != null)
            {
                _snapshotPreviewForm.WindowState = WindowState;
            }
        }

        private void OpenInExplorerToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var row = GetSelectedDataGridViewRows(dataGridView1).FirstOrDefault();
            if (row == null)
            {
                return;
            }

            Process.Start("explorer.exe", $"/select, \"{(string)row.Cells["PathColumn"].Value}\"");
        }

        private async void DataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
        {
            var dataGridView = (DataGridView)sender;

            if (_snapshotPreviewForm == null)
            {
                _snapshotPreviewForm = new SnapshotPreviewForm(this, _snapshotDatabase);
                _snapshotPreviewForm.Owner = this;
                _snapshotPreviewForm.Closing += SnapshotPreviewForm_Closing;
                _snapshotPreviewForm.Show();
            }

            var row = GetSelectedDataGridViewRows(dataGridView).FirstOrDefault();
            if (row == null)
            {
                return;
            }

            var hash = (string)row.Cells["HashColumn"].Value;

            var snapshotPreview =
                await Task.Run(async () => await _snapshotDatabase.RetrievePreviewAsync(hash, _cancellationToken),
                    _cancellationToken);

            if (snapshotPreview == null)
            {
                return;
            }

            PreviewRetrieved?.Invoke(this, new PreviewRetrievedEventArgs(snapshotPreview));
        }

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

            _snapshotPreviewForm.Dispose();
            _snapshotPreviewForm = null;
        }

        private async void NoneToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Failed to remove color from row.");

                    toolStripStatusLabel1.Text =
                        $"Could not remove color from {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                rowProgress.Row.DefaultCellStyle.BackColor = Color.Empty;

                toolStripStatusLabel1.Text =
                    $"Removed color from {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => RemoveColorFiles(rows, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        private async void ColorToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var toolStripMenuItem = (ToolStripMenuItem)sender;
            var color = toolStripMenuItem.BackColor;

            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Unable to color row.");

                    toolStripStatusLabel1.Text =
                        $"Could not color {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                rowProgress.Row.DefaultCellStyle.BackColor = color;

                toolStripStatusLabel1.Text =
                    $"Colored {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => ColorFiles(rows, color, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        private async void DeleteToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (_cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Unable to delete row.");

                    toolStripStatusLabel1.Text =
                        $"Could not remove {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                toolStripStatusLabel1.Text =
                    $"Removed {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();

                dataGridView1.Rows.Remove(rowProgress.Row);
            });

            await Task.Run(() => DeleteFiles(rows, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        private async void DeleteFastToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);

            try
            {
                await DeleteFilesFast(rows, _cancellationToken);

                foreach (var row in rows)
                {
                    dataGridView1.Rows.Remove(row);
                }
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Unable to remove rows.");
            }
        }

        private void SnapshotManager_SnapshotCreate(object sender, SnapshotCreateEventArgs e)
        {
            switch (e)
            {
                case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
                    dataGridView1.InvokeIfRequired(dataGridView =>
                    {
                        var index = dataGridView.Rows.Add();

                        dataGridView.Rows[index].Cells["TimeColumn"].Value =
                            DateTime.Parse(snapshotCreateSuccessEventArgs.Time);
                        dataGridView.Rows[index].Cells["NameColumn"].Value = snapshotCreateSuccessEventArgs.Name;
                        dataGridView.Rows[index].Cells["PathColumn"].Value = snapshotCreateSuccessEventArgs.Path;
                        dataGridView.Rows[index].Cells["HashColumn"].Value = snapshotCreateSuccessEventArgs.Hash;
                        dataGridView.Rows[index].DefaultCellStyle.BackColor = snapshotCreateSuccessEventArgs.Color;

                        dataGridView.Sort(dataGridView.Columns["TimeColumn"], ListSortDirection.Descending);
                    });
                    break;
                case SnapshotCreateFailureEventArgs snapshotCreateFailure:
                    Log.Warning(snapshotCreateFailure.Exception, "Could not create snapshot.");
                    break;
            }
        }

        private void RevertToThisToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _mainForm.InvokeIfRequired(async form =>
            {
                var fileSystemWatchers = new List<FileSystemWatcherState>();
                var watchPaths = new HashSet<string>();
                // Temporary disable all filesystem watchers that are watching the selected file directory.
                foreach (var row in GetSelectedDataGridViewRows(dataGridView1))
                {
                    var path = (string)row.Cells["PathColumn"].Value;

                    foreach (var fileSystemWatcher in form.FileSystemWatchers)
                    {
                        if (!path.IsPathEqual(fileSystemWatcher.Path) && 
                            !path.IsSubPathOf(fileSystemWatcher.Path))
                        {
                            continue;
                        }

                        if (watchPaths.Contains(fileSystemWatcher.Path))
                        {
                            continue;
                        }

                        fileSystemWatchers.Add(new FileSystemWatcherState(fileSystemWatcher));

                        fileSystemWatcher.EnableRaisingEvents = false;

                        watchPaths.Add(fileSystemWatcher.Path);
                    }
                }

                try
                {
                    var rows = GetSelectedDataGridViewRows(dataGridView1);

                    var count = rows.Count;

                    toolStripProgressBar1.Minimum = 0;
                    toolStripProgressBar1.Maximum = count;

                    var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
                    {
                        if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                        {
                            Log.Error(rowProgressFailure.Exception, "Could not revert to snapshot.");

                            toolStripStatusLabel1.Text =
                                $"Could not revert {rowProgress.Row.Cells["NameColumn"].Value}...";
                            toolStripProgressBar1.Value = rowProgress.Index + 1;

                            statusStrip1.Update();

                            return;
                        }

                        toolStripStatusLabel1.Text =
                            $"Reverted {rowProgress.Row.Cells["NameColumn"].Value}...";
                        toolStripProgressBar1.Value = rowProgress.Index + 1;

                        statusStrip1.Update();
                    });

                    await Task.Run(() => RevertFile(rows, progress, _cancellationToken), _cancellationToken);

                    if (_cancellationToken.IsCancellationRequested)
                    {
                        toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                        toolStripStatusLabel1.Text = "Done.";
                    }
                }
                catch (Exception exception)
                {
                    Log.Error(exception, "Could not update data grid view.");
                }
                finally
                {
                    // Restore initial state.
                    foreach (var fileSystemWatcherState in fileSystemWatchers)
                    {
                        foreach (var fileSystemWatcher in form.FileSystemWatchers)
                        {
                            if (fileSystemWatcherState.FileSystemWatcher == fileSystemWatcher)
                            {
                                fileSystemWatcher.EnableRaisingEvents = fileSystemWatcherState.State;
                            }
                        }
                    }
                }
            });
        }

        private async void SnapshotManagerForm_Load(object sender, EventArgs e)
        {
            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = (int)await _snapshotDatabase.CountSnapshotsAsync(_cancellationToken);

            var snapshotQueue = new ConcurrentQueue<Snapshot>();

            void IdleHandler(object idleHandlerSender, EventArgs idleHandlerArgs)
            {
                try
                {
                    if (snapshotQueue.IsEmpty)
                    {
                        Application.Idle -= IdleHandler;

                        dataGridView1.Sort(dataGridView1.Columns["TimeColumn"], ListSortDirection.Descending);
                        toolStripStatusLabel1.Text = "Done.";
                    }

                    if (!snapshotQueue.TryDequeue(out var snapshot))
                    {
                        return;
                    }

                    var index = dataGridView1.Rows.Add();

                    dataGridView1.Rows[index].Cells["TimeColumn"].Value = DateTime.Parse(snapshot.Time);
                    dataGridView1.Rows[index].Cells["NameColumn"].Value = snapshot.Name;
                    dataGridView1.Rows[index].Cells["PathColumn"].Value = snapshot.Path;
                    dataGridView1.Rows[index].Cells["HashColumn"].Value = snapshot.Hash;
                    dataGridView1.Rows[index].DefaultCellStyle.BackColor = snapshot.Color;

                    toolStripStatusLabel1.Text = $"Loaded {snapshot.Name}...";

                    toolStripProgressBar1.Increment(1);

                    statusStrip1.Update();
                }
                catch (Exception exception)
                {
                    Log.Error(exception, "Could not update data grid view.");
                }
            }
            
            try
            {
                await foreach (var snapshot in _snapshotDatabase.LoadSnapshotsAsync(_cancellationToken).WithCancellation(_cancellationToken))
                {
                    snapshotQueue.Enqueue(snapshot);
                }

                Application.Idle += IdleHandler;
            }
            catch (Exception exception)
            {
                Application.Idle -= IdleHandler;

                Log.Error(exception, "Unable to load snapshots.");
            }
        }

        private void SnapshotManagerForm_Closing(object sender, FormClosingEventArgs e)
        {
            _cancellationTokenSource.Cancel();

            if (_snapshotPreviewForm != null)
            {
                _snapshotPreviewForm.Close();
                _snapshotPreviewForm = null;
            }

            if (_hexViewForm != null)
            {
                _hexViewForm.Close();
                _hexViewForm = null;
            }
        }

        private void DataGridView1_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
                return;
            }

            e.Effect = DragDropEffects.None;
        }

        private void CreateSnapshots(IReadOnlyList<string> files, Bitmap screenCapture, TrackedFolders.TrackedFolders trackedFolders, IProgress<CreateSnapshotProgress> progress, CancellationToken cancellationToken)
        {
            Parallel.ForEach(files, new ParallelOptions { MaxDegreeOfParallelism = 512 }, async file =>
            {
                var color = Color.Empty;
                if (_mainForm.TrackedFolders.TryGet(file, out var folder))
                {
                    color = folder.Color;
                }

                var fileInfo = File.GetAttributes(file);
                if (fileInfo.HasFlag(FileAttributes.Directory))
                {
                    foreach (var directoryFile in Directory.GetFiles(file, "*.*", SearchOption.AllDirectories))
                    {
                        var name = Path.GetFileName(directoryFile);
                        var path = Path.Combine(Path.GetDirectoryName(directoryFile), name);

                        try
                        {
                            await _snapshotDatabase.CreateSnapshotAsync(name, path, screenCapture, color,
                                _cancellationToken);

                            progress.Report(new CreateSnapshotProgressSuccess(file));
                        }
                        catch (Exception exception)
                        {
                            progress.Report(new CreateSnapshotProgressFailure(file, exception));
                        }
                    }

                    return;
                }

                var fileName = Path.GetFileName(file);
                var pathName = Path.Combine(Path.GetDirectoryName(file), fileName);

                try
                {
                    await _snapshotDatabase.CreateSnapshotAsync(fileName, pathName, screenCapture, color,
                        _cancellationToken);

                    progress.Report(new CreateSnapshotProgressSuccess(file));
                }
                catch (Exception exception)
                {
                    progress.Report(new CreateSnapshotProgressFailure(file, exception));
                }
            });
        }
        private async void DataGridView1_DragDrop(object sender, DragEventArgs e)
        {
            if (!e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                return;
            }

            var files = (string[])e.Data.GetData(DataFormats.FileDrop);

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = files.Length;
            toolStripStatusLabel1.Text = "Snapshotting files...";

            var screenCapture = ScreenCapture.Capture((CaptureMode)_mainForm.Configuration.CaptureMode);

            var progress = new Progress<CreateSnapshotProgress>(createSnapshotProgress =>
            {
                switch (createSnapshotProgress)
                {
                    case CreateSnapshotProgressSuccess createSnapshotProgressSuccess:
                        toolStripStatusLabel1.Text = $"Snapshot taken of {createSnapshotProgressSuccess.File}.";
                        break;
                    case CreateSnapshotProgressFailure createSnapshotProgressFailure:
                        if (createSnapshotProgressFailure.Exception is SQLiteException { ResultCode: SQLiteErrorCode.Constraint })
                        {
                            toolStripStatusLabel1.Text = $"Snapshot of file {createSnapshotProgressFailure.File} already exists.";
                            break;
                        }

                        toolStripStatusLabel1.Text = $"Could not snapshot file {createSnapshotProgressFailure.File}";
                        Log.Warning(createSnapshotProgressFailure.Exception, $"Could not snapshot file {createSnapshotProgressFailure.File}");
                        break;
                }

                toolStripProgressBar1.Increment(1);
                statusStrip1.Update();
            });

            await Task.Factory.StartNew( () =>
            {
                CreateSnapshots(files, screenCapture, _mainForm.TrackedFolders, progress, _cancellationToken);
            }, _cancellationToken);
        }

        private async void FileToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var dialog = new CommonOpenFileDialog();
            if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                var screenCapture = ScreenCapture.Capture((CaptureMode)_mainForm.Configuration.CaptureMode);

                var fileName = Path.GetFileName(dialog.FileName);
                var directory = Path.GetDirectoryName(dialog.FileName);
                var pathName = Path.Combine(directory, fileName);

                var color = Color.Empty;
                if (_mainForm.TrackedFolders.TryGet(directory, out var folder))
                {
                    color = folder.Color;
                }

                try
                {
                    await _snapshotDatabase.CreateSnapshotAsync(fileName, pathName, screenCapture, color,
                        _cancellationToken);
                }
                catch (SQLiteException exception)
                {
                    if (exception.ResultCode == SQLiteErrorCode.Constraint)
                    {
                        Log.Information(exception, "Snapshot already exists.");
                    }
                }
                catch (Exception exception)
                {
                    Log.Warning(exception, "Could not create snapshot.");
                }
            }
        }

        private async void DirectoryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var dialog = new CommonOpenFileDialog { IsFolderPicker = true };
            if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                var screenCapture = ScreenCapture.Capture((CaptureMode)_mainForm.Configuration.CaptureMode);
                foreach (var directoryFile in Directory.GetFiles(dialog.FileName, "*.*", SearchOption.AllDirectories))
                {
                    var name = Path.GetFileName(directoryFile);
                    var directory = Path.GetDirectoryName(directoryFile);
                    var path = Path.Combine(directory, name);

                    var color = Color.Empty;
                    if (_mainForm.TrackedFolders.TryGet(directory, out var folder))
                    {
                        color = folder.Color;
                    }

                    try
                    {
                        await _snapshotDatabase.CreateSnapshotAsync(name, path, screenCapture, color, _cancellationToken);
                    }
                    catch (SQLiteException exception)
                    {
                        if (exception.ResultCode == SQLiteErrorCode.Constraint)
                        {
                            Log.Information(exception, "Snapshot already exists.");
                        }
                    }
                    catch (Exception exception)
                    {
                        Log.Warning(exception, "Could not create snapshot.");
                    }
                }
            }
        }

        private async void RelocateToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var commonOpenFileDialog = new CommonOpenFileDialog
            {
                InitialDirectory = _mainForm.Configuration.LastFolder,
                IsFolderPicker = true
            };

            if (commonOpenFileDialog.ShowDialog() != CommonFileDialogResult.Ok)
            {
                return;
            }

            _mainForm.Configuration.LastFolder = commonOpenFileDialog.FileName;
            _mainForm.ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
                async () => await _mainForm.SaveConfiguration(), _cancellationToken);

            var directory = commonOpenFileDialog.FileName;

            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                var path = Path.Combine(directory,
                    (string)rowProgress.Row.Cells["NameColumn"].Value);

                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Could not relocate snapshot.");

                    toolStripStatusLabel1.Text =
                        $"Could not relocate {rowProgress.Row.Cells["NameColumn"].Value} to {path}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                rowProgress.Row.Cells["PathColumn"].Value = path;

                toolStripStatusLabel1.Text =
                    $"Relocated {rowProgress.Row.Cells["NameColumn"].Value} to {path}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => RelocateFiles(rows, directory, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        private async void NoteToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (_snapshotNote != null)
            {
                return;
            }

            var row = GetSelectedDataGridViewRows(dataGridView1).FirstOrDefault();
            if (row == null)
            {
                return;
            }

            try
            {
                var snapshotPreview = await _snapshotDatabase.RetrievePreviewAsync(
                    (string)row.Cells["HashColumn"].Value, _cancellationToken);

                if (snapshotPreview == null)
                {
                    return;
                }

                _snapshotNote = new SnapshotNoteForm(this, snapshotPreview);
                _snapshotNote.Owner = this;
                _snapshotNote.SaveNote += SnapshotNote_SaveNote;
                _snapshotNote.Closing += SnapshotNote_Closing;
                _snapshotNote.Show();
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Could not open notes form.");
            }
        }

        private async void SnapshotNote_SaveNote(object sender, SaveNoteEventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Could not update note for snapshot.");

                    toolStripStatusLabel1.Text =
                        $"Could not update note for {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                toolStripStatusLabel1.Text =
                    $"Updated note for {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => UpdateNote(rows, e.Note, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

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

            _snapshotNote.Closing -= SnapshotNote_Closing;
            _snapshotNote.Close();
            _snapshotNote = null;
        }

        private async void ViewHexToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);
            var row = rows.FirstOrDefault();
            if (row == null)
            {
                return;
            }

            var hash = (string)row.Cells["HashColumn"].Value;

            using (var memoryStream = await _snapshotDatabase.RetrieveFileStreamAsync(hash, _cancellationToken))
            {
                if (memoryStream == null)
                {
                    return;
                }

                if (_hexViewForm != null)
                {
                    _hexViewForm.UpdateData(memoryStream.ToArray());
                    _hexViewForm.Activate();
                    return;
                }

                _hexViewForm = new HexViewForm(_snapshotDatabase, hash, memoryStream.ToArray());
                _hexViewForm.Owner = this;
                _hexViewForm.Closing += HexViewForm_Closing;
                _hexViewForm.SaveData += HexViewForm_SaveData;

                _hexViewForm.Show();
            }
        }

        private async void HexViewForm_SaveData(object sender, SaveDataEventArgs e)
        {
            var hash = await _snapshotDatabase.UpdateFileAsync(e.Hash, e.Data, _cancellationToken);

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

            dataGridView1.InvokeIfRequired(dataGridView =>
            {
                // Update the hash in the datagridview.
                var removeRows = new List<DataGridViewRow>();
                foreach (var row in dataGridView.Rows.OfType<DataGridViewRow>())
                {
                    if ((string)row.Cells["HashColumn"].Value == hash)
                    {
                        removeRows.Add(row);
                    }

                    if ((string)row.Cells["HashColumn"].Value != e.Hash)
                    {
                        continue;
                    }

                    row.Cells["HashColumn"].Value = hash;
                }

                // Remove rows that might have the same hash.
                foreach (var row in removeRows)
                {
                    dataGridView.Rows.Remove(row);
                }
            });
        }

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

            _hexViewForm.SaveData -= HexViewForm_SaveData;
            _hexViewForm.Closing -= HexViewForm_Closing;
            _hexViewForm.Close();
            _hexViewForm = null;
        }

        private async void FileToolStripMenuItem2_Click(object sender, EventArgs e)
        {
            var commonOpenFileDialog = new CommonOpenFileDialog
            {
                InitialDirectory = _mainForm.Configuration.LastFolder,
                IsFolderPicker = true
            };

            if (commonOpenFileDialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                _mainForm.Configuration.LastFolder = commonOpenFileDialog.FileName;
                _mainForm.ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
                    async () => await _mainForm.SaveConfiguration(), _cancellationToken);

                var directory = commonOpenFileDialog.FileName;

                var rows = GetSelectedDataGridViewRows(dataGridView1);

                var count = rows.Count;

                toolStripProgressBar1.Minimum = 0;
                toolStripProgressBar1.Maximum = count;

                var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
                {
                    var fileInfo =
                        new FileInfo((string)rowProgress.Row.Cells["NameColumn"].Value);
                    var file = fileInfo.Name;
                    var path = Path.Combine(directory, file);

                    if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                    {
                        Log.Error(rowProgressFailure.Exception, "Could not save snapshot.");

                        toolStripStatusLabel1.Text =
                            $"Could not save snapshot {rowProgress.Row.Cells["NameColumn"].Value} to {path}...";
                        toolStripProgressBar1.Value = rowProgress.Index + 1;

                        statusStrip1.Update();

                        return;
                    }

                    toolStripStatusLabel1.Text =
                        $"Saved {rowProgress.Row.Cells["NameColumn"].Value} to {path}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();
                });

                await Task.Run(() => SaveFilesTo(rows, directory, progress, _cancellationToken), _cancellationToken);

                if (_cancellationToken.IsCancellationRequested)
                {
                    toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                    toolStripStatusLabel1.Text = "Done.";
                }
            }
        }

        private async void DirectoryToolStripMenuItem2_Click(object sender, EventArgs e)
        {
            var select = GetSelectedDataGridViewRows(dataGridView1).FirstOrDefault();

            if (select == null)
            {
                return;
            }

            // C:\aa\bbb\dd.txt
            var path = (string)select.Cells["PathColumn"].Value;

            // C:\aa\bbb\
            var basePath = Path.GetDirectoryName(path);

            var dialog = new CommonOpenFileDialog { IsFolderPicker = true };
            if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
            {
                //Log.Information(dialog.FileName);
                var rows = GetAllDataGridViewRows(dataGridView1);
                var count = rows.Count;

                toolStripProgressBar1.Minimum = 0;
                toolStripProgressBar1.Maximum = count;
                var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
                {
                    if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                    {
                        Log.Error(rowProgressFailure.Exception, "Could not save file.");

                        toolStripStatusLabel1.Text =
                            $"Could not save file {rowProgress.Row.Cells["NameColumn"].Value} to {basePath}...";
                        toolStripProgressBar1.Value = rowProgress.Index + 1;

                        statusStrip1.Update();

                        return;
                    }

                    toolStripStatusLabel1.Text =
                        $"Saved {rowProgress.Row.Cells["NameColumn"].Value} to {basePath}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();
                });

                await Task.Run(() => SaveDirectoryTo(rows, basePath, dialog.FileName, progress, _cancellationToken),
                    _cancellationToken);

                if (_cancellationToken.IsCancellationRequested)
                {
                    toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                    toolStripStatusLabel1.Text = "Done.";
                }
            }
        }

        private void TextBox1_TextChanged(object sender, EventArgs e)
        {
            _searchTextBoxChangedContinuation.Schedule(TimeSpan.FromSeconds(1), () =>
            {
                textBox1.InvokeIfRequired(textBox =>
                {
                    var search = textBox.Text;

                    dataGridView1.InvokeIfRequired(dataGridView =>
                    {
                        foreach (var row in GetAllDataGridViewRows(dataGridView))
                        {
                            if(row.Cells["PathColumn"].Value == null)
                            {
                                continue;
                            }

                            switch (((string)row.Cells["PathColumn"].Value).IndexOf(search,
                                        StringComparison.OrdinalIgnoreCase))
                            {
                                case -1:
                                    row.Visible = false;
                                    break;
                                default:
                                    row.Visible = true;
                                    break;
                            }
                        }
                    });
                });
            }, _cancellationToken);
        }

        private async void RecomputeHashesToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Could not recompute hash for snapshot.");

                    toolStripStatusLabel1.Text =
                        $"Could not recompute hash for {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                toolStripStatusLabel1.Text =
                    $"Recomputed hash for {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => RecomputeHashes(rows, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        private async void NormalizeDateTimeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Could not normalize date-time for snapshot.");

                    toolStripStatusLabel1.Text =
                        $"Could not normalize date-time for {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                toolStripStatusLabel1.Text =
                    $"Normalized date-time for {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => NormalizeDateTime(rows, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        #endregion

        #region Private Methods

        private async Task DeleteFiles(IReadOnlyList<DataGridViewRow> rows, IProgress<DataGridViewRowProgress> progress,
                                       CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var index = 0; index < count && !cancellationToken.IsCancellationRequested; ++index)
            {
                try
                {
                    await _snapshotDatabase.RemoveFileAsync((string)rows[index].Cells["HashColumn"].Value,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[index], index));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[index], index, exception));
                }
            }
        }

        private async Task DeleteFilesFast(IReadOnlyList<DataGridViewRow> rows, CancellationToken cancellationToken)
        {
            var hashes = rows.Select(row => (string)row.Cells["HashColumn"].Value);

            await _snapshotDatabase.RemoveFileFastAsync(hashes, cancellationToken);
        }

        private async Task UpdateNote(IReadOnlyList<DataGridViewRow> rows, string note,
                                      IProgress<DataGridViewRowProgress> progress, CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    await _snapshotDatabase.UpdateNoteAsync((string)rows[i].Cells["HashColumn"].Value, note,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private static List<DataGridViewRow> GetSelectedDataGridViewRows(DataGridView dataGridView)
        {
            return dataGridView.SelectedRows.OfType<DataGridViewRow>().ToList();
        }

        private static List<DataGridViewRow> GetAllDataGridViewRows(DataGridView dataGridView)
        {
            return dataGridView.Rows.OfType<DataGridViewRow>().ToList();
        }

        private async Task RemoveColorFiles(IReadOnlyList<DataGridViewRow> rows,
                                            IProgress<DataGridViewRowProgress> progress,
                                            CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    await _snapshotDatabase.RemoveColorAsync((string)rows[i].Cells["HashColumn"].Value,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async Task ColorFiles(IReadOnlyList<DataGridViewRow> rows, Color color,
                                      IProgress<DataGridViewRowProgress> progress, CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    await _snapshotDatabase.UpdateColorAsync((string)rows[i].Cells["HashColumn"].Value, color,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async Task RevertFile(IReadOnlyList<DataGridViewRow> rows, IProgress<DataGridViewRowProgress> progress,
                                      CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    await _snapshotDatabase.RevertFileAsync((string)rows[i].Cells["NameColumn"].Value,
                        (string)rows[i].Cells["HashColumn"].Value,
                        cancellationToken, _mainForm.Configuration.AtomicOperations);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async void SaveFilesTo(IReadOnlyList<DataGridViewRow> rows, string directory,
                                       IProgress<DataGridViewRowProgress> progress, CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    var fileInfo = new FileInfo((string)rows[i].Cells["NameColumn"].Value);
                    var file = fileInfo.Name;
                    var path = Path.Combine(directory, file);

                    await _snapshotDatabase.SaveFileAsync(path, (string)rows[i].Cells["HashColumn"].Value,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async Task RelocateFiles(IReadOnlyList<DataGridViewRow> rows, string directory,
                                         IProgress<DataGridViewRowProgress> progress,
                                         CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    var path = Path.Combine(directory, (string)rows[i].Cells["NameColumn"].Value);

                    await _snapshotDatabase.RelocateFileAsync((string)rows[i].Cells["HashColumn"].Value, path,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async void RecomputeHashes(IReadOnlyList<DataGridViewRow> rows,
                                           IProgress<DataGridViewRowProgress> progress,
                                           CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    using (var memoryStream =
                           await _snapshotDatabase.RetrieveFileStreamAsync((string)rows[i].Cells["HashColumn"].Value,
                               cancellationToken))
                    {
                        if (memoryStream == null)
                        {
                            continue;
                        }

                        using (var md5 = MD5.Create())
                        {
                            var recomputedHash = md5.ComputeHash(memoryStream);
                            var hashHex = BitConverter.ToString(recomputedHash).Replace("-", "")
                                .ToLowerInvariant();

                            await _snapshotDatabase.UpdateHashAsync((string)rows[i].Cells["HashColumn"].Value, hashHex,
                                cancellationToken);

                            rows[i].Cells["HashColumn"].Value = hashHex;

                            progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                        }
                    }
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async Task SaveDirectoryTo(IReadOnlyList<DataGridViewRow> rows, string basePath, string targetPath,
                                           IProgress<DataGridViewRowProgress> progress,
                                           CancellationToken cancellationToken)
        {
            var store = new HashSet<string>();

            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    // C:\aa\bbb\fff\gg.txt
                    var rowPath = (string)rows[i].Cells["PathColumn"].Value;
                    if (store.Contains(rowPath))
                    {
                        continue;
                    }

                    // C:\aa\bbb\fff\gg.txt subpath C:\aa\bbb\
                    if (!rowPath.IsPathEqual(basePath) && 
                        !rowPath.IsSubPathOf(basePath))
                    {
                        continue;
                    }

                    var rootPath = new DirectoryInfo(basePath).Name;
                    var relPath = rowPath.Remove(0, basePath.Length).Trim('\\');
                    var newPath = Path.Combine(targetPath, rootPath, relPath);

                    var hash = (string)rows[i].Cells["HashColumn"].Value;
                    await _snapshotDatabase.SaveFileAsync(newPath, hash, cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));

                    if (!store.Contains(rowPath))
                    {
                        store.Add(rowPath);
                    }
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async Task NormalizeDateTime(IReadOnlyList<DataGridViewRow> rows,
                                             IProgress<DataGridViewRowProgress> progress,
                                             CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    await _snapshotDatabase.NormalizeTimeAsync((string)rows[i].Cells["HashColumn"].Value,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        private async void deleteToolStripMenuItem1_Click(object sender, EventArgs e)
        {
            var toolStripMenuItem = (ToolStripMenuItem)sender;

            var rows = GetSelectedDataGridViewRows(dataGridView1);

            var count = rows.Count;

            toolStripProgressBar1.Minimum = 0;
            toolStripProgressBar1.Maximum = count;

            var progress = new Progress<DataGridViewRowProgress>(rowProgress =>
            {
                if (rowProgress is DataGridViewRowProgressFailure rowProgressFailure)
                {
                    Log.Error(rowProgressFailure.Exception, "Unable to delete screenshot.");

                    toolStripStatusLabel1.Text =
                        $"Could not delete screenshot for {rowProgress.Row.Cells["NameColumn"].Value}...";
                    toolStripProgressBar1.Value = rowProgress.Index + 1;

                    statusStrip1.Update();

                    return;
                }

                toolStripStatusLabel1.Text =
                    $"Colored {rowProgress.Row.Cells["NameColumn"].Value}...";
                toolStripProgressBar1.Value = rowProgress.Index + 1;

                statusStrip1.Update();
            });

            await Task.Run(() => DeleteScreenshots(rows, progress, _cancellationToken), _cancellationToken);

            if (_cancellationToken.IsCancellationRequested)
            {
                toolStripProgressBar1.Value = toolStripProgressBar1.Maximum;
                toolStripStatusLabel1.Text = "Done.";
            }
        }

        private async Task DeleteScreenshots(IReadOnlyList<DataGridViewRow> rows,
                                             IProgress<DataGridViewRowProgress> progress,
                                             CancellationToken cancellationToken)
        {
            var count = rows.Count;

            for (var i = 0; i < count && !cancellationToken.IsCancellationRequested; ++i)
            {
                try
                {
                    await _snapshotDatabase.DeleteScreenshotAsync((string)rows[i].Cells["HashColumn"].Value,
                        cancellationToken);

                    progress.Report(new DataGridViewRowProgressSuccess(rows[i], i));
                }
                catch (Exception exception)
                {
                    progress.Report(new DataGridViewRowProgressFailure(rows[i], i, exception));
                }
            }
        }

        #endregion

        private void DataGridView1_RowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
        {
            DataGridView dataGridView = sender as DataGridView;
            foreach (DataGridViewCell cell in dataGridView.Rows[e.RowIndex].Cells)
            {
                if (cell.Selected == false) { continue; }
                var bgColorCell = Color.White;
                if (cell.Style.BackColor != Color.Empty) { bgColorCell = cell.Style.BackColor; }
                else if (cell.InheritedStyle.BackColor != Color.Empty) { bgColorCell = cell.InheritedStyle.BackColor; }
                cell.Style.SelectionBackColor = MixColor(bgColorCell, Color.FromArgb(0, 150, 255), 10, 4);
            }
        }

        //Mix two colors
        //Example: Steps=10 & Position=4 makes Color2 mix 40% into Color1
        /// <summary>
        /// Mix two colors.
        /// </summary>
        /// <param name="Color1"></param>
        /// <param name="Color2"></param>
        /// <param name="Steps"></param>
        /// <param name="Position"></param>
        /// <example>Steps=10 & Positon=4 makes Color2 mix 40% into Color1</example>
        /// <remarks>https://stackoverflow.com/questions/38337849/transparent-selectionbackcolor-for-datagridview-cell</remarks>
        /// <returns></returns>
        public static Color MixColor(Color Color1, Color Color2, int Steps, int Position)
        {
            if (Position <= 0 || Steps <= 1) { return Color1; }
            if (Position >= Steps) { return Color2; }
            return Color.FromArgb(
                Color1.R + ((Color2.R - Color1.R) / Steps * Position),
                Color1.G + ((Color2.G - Color1.G) / Steps * Position),
                Color1.B + ((Color2.B - Color1.B) / Steps * Position)
                );
        }
    }
}

Generated by GNU Enscript 1.6.5.90.