Horizon – Rev 17

Subversion Repositories:
Rev:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data.SQLite;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows;
using System.Windows.Forms;
using Configuration;
using Horizon.Database;
using Horizon.Notifications.Gotify;
using Horizon.Snapshots;
using Horizon.Utilities;
using Horizon.Utilities.Serialization;
using Mono.Zeroconf;
using NetSparkleUpdater;
using NetSparkleUpdater.Enums;
using NetSparkleUpdater.SignatureVerifiers;
using NetSparkleUpdater.UI.WinForms;
using Newtonsoft.Json;
using Serilog;
using TrackedFolders;
using WatsonTcp;
using Newtonsoft;
using static Horizon.Utilities.Networking.Miscellaneous;
using CaptureMode = Configuration.CaptureMode;
using Path = System.IO.Path;
using System.Text;
using Tesseract;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Horizon.Searching;

namespace Horizon
{
    public partial class MainForm : Form
    {

        #region Static Fields and Constants

        private static SemaphoreSlim _changedFilesLock;

        private static HashSet<string> _changedFiles;

        private static ScheduledContinuation _changedFilesContinuation;

        private static readonly LogMemorySink _memorySink = new LogMemorySink();

        #endregion

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

        private ScheduledContinuation _changedConfigurationContinuation;

        private ScheduledContinuation _trackedFoldersChangedContinuation;

        private Configuration.Configuration _configuration;

        private TrackedFolders.TrackedFolders _trackedFolders;

        private readonly ConcurrentQueue<FileSystemWatcher> _fileSystemWatchers;

        private readonly CancellationToken _cancellationToken;

        private static HttpClient _httpClient;

        private readonly CancellationTokenSource _cancellationTokenSource;

        private AboutForm _aboutForm;

        private ManageFoldersForm _manageFoldersForm;

        private readonly SnapshotDatabase _snapshotDatabase;

        private SnapshotManagerForm _snapshotManagerForm;

        private SparkleUpdater _sparkle;

        private LogViewForm _logViewForm;

        private static JsonSerializer _jsonSerializer;

        private RegisterService _horizonDiscoveryService;

        private WatsonTcpServer _horizonNetworkShare;

        private NotifyFilters _fileSystemWatchersNotifyFilters = NotifyFilters.LastWrite | NotifyFilters.Attributes;

        private readonly bool _memorySinkEnabled = true;

        private SearchEngine _searchEngine;

        #endregion

        #region Constructors, Destructors and Finalizers
        public MainForm()
        {
            InitializeComponent();

            _cancellationTokenSource = new CancellationTokenSource();
            _cancellationToken = _cancellationTokenSource.Token;

            _jsonSerializer = new JsonSerializer();

            _httpClient = new HttpClient();

            _trackedFoldersChangedContinuation = new ScheduledContinuation();

            _changedFilesLock = new SemaphoreSlim(1, 1);
            _fileSystemWatchers = new ConcurrentQueue<FileSystemWatcher>();

            _changedFiles = new HashSet<string>();
            _changedFilesContinuation = new ScheduledContinuation();

            _changedConfigurationContinuation = new ScheduledContinuation();

        }

        public MainForm(Mutex mutex) : this()
        {
            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();

            _snapshotDatabase = new SnapshotDatabase(_cancellationToken);
            _snapshotDatabase.SnapshotRevert += SnapshotDatabase_SnapshotRevert;
            _snapshotDatabase.SnapshotCreate += SnapshotDatabase_SnapshotCreateAsync;
            _snapshotDatabase.SnapshotTransferReceived += SnapshotDatabase_SnapshotTransferReceived;
            _snapshotDatabase.SnapshotNoteUpdate += _snapshotDatabase_SnapshotNoteUpdate;
            _snapshotDatabase.SnapshotDataUpdate += _snapshotDatabase_SnapshotDataUpdate;

            _trackedFolders = new TrackedFolders.TrackedFolders();
            _trackedFolders.Folder.CollectionChanged += Folder_CollectionChanged;
        }

        /// <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)
            {
                toolStripMenuItem3.DropDown.Closing -= toolStropMenuItem3DropDown_Closing;
                eventsToolStripMenuItem.DropDown.Closing -= eventsToolStripMenuItem_Closing;

                components.Dispose();
            }

            _cancellationTokenSource.Cancel();
            _configuration.PropertyChanged -= _configuration_PropertyChanged;

            _snapshotDatabase.SnapshotNoteUpdate -= _snapshotDatabase_SnapshotNoteUpdate;
            _snapshotDatabase.SnapshotDataUpdate -= _snapshotDatabase_SnapshotDataUpdate;
            _snapshotDatabase.SnapshotRevert -= SnapshotDatabase_SnapshotRevert;
            _snapshotDatabase.SnapshotCreate -= SnapshotDatabase_SnapshotCreateAsync;
            _snapshotDatabase.SnapshotTransferReceived -= SnapshotDatabase_SnapshotTransferReceived;

            _snapshotDatabase.Dispose();
       
            base.Dispose(disposing);
        }

        private void eventsToolStripMenuItem_Closing(object sender, ToolStripDropDownClosingEventArgs e)
        {
            if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
            {
                e.Cancel = true;
            }
        }

        private void toolStropMenuItem3DropDown_Closing(object sender, ToolStripDropDownClosingEventArgs e)
        {
            if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
            {
                e.Cancel = true;
            }
        }

        #endregion

        #region Event Handlers

        private void autoNotesToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _configuration.AutoNotes = ((ToolStripMenuItem)sender).Checked;
        }

        private void gotfyToolStripTextBox_TextChanged(object sender, EventArgs e)
        {
            var toolStripTextBox = ((ToolStripTextBox)sender);

            _configuration.GotifyURL = toolStripTextBox.Text;
        }

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

            var text = toolStripMenuItem.Text;
            var state = toolStripMenuItem.CheckState;

            foreach (var flag in Enum.GetNames(typeof(NotifyEvent)))
            {
                if (string.Equals(flag, text, StringComparison.OrdinalIgnoreCase))
                {
                    if (Enum.TryParse<NotifyEvent>(flag, true, out var setting))
                    {
                        switch (state)
                        {
                            case CheckState.Checked:
                                _configuration.NotifyEvents = _configuration.NotifyEvents | setting;
                                break;
                            case CheckState.Unchecked:
                                _configuration.NotifyEvents = _configuration.NotifyEvents & ~setting;
                                break;
                        }

                    }
                }
            }
        }

        private void gotifyToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
        {
            _configuration.EnableGotify = ((ToolStripMenuItem)sender).Checked;
        }

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

            var text = toolStripMenuItem.Text;
            var state = toolStripMenuItem.CheckState;

            foreach (var flag in Enum.GetNames(typeof(NotifyFilters)))
            {
                if (string.Equals(flag, text, StringComparison.OrdinalIgnoreCase))
                {
                    if (Enum.TryParse<NotifyFilters>(flag, true, out var setting))
                    {
                        switch (state)
                        {
                            case CheckState.Checked:
                                _fileSystemWatchersNotifyFilters = _fileSystemWatchersNotifyFilters | setting;
                                break;
                            case CheckState.Unchecked:
                                _fileSystemWatchersNotifyFilters = _fileSystemWatchersNotifyFilters & ~setting;
                                break;
                        }
                        
                    }
                }
            }

            _configuration.NotifyFilters = _fileSystemWatchersNotifyFilters;
        }

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

            switch (toolStripMenuItem.CheckState)
            {
                case CheckState.Checked:
                    var freePort = GetAvailableTcpPort();

                    _horizonNetworkShare = new WatsonTcpServer("0.0.0.0", freePort);
                    _horizonNetworkShare.Events.ClientConnected += Events_ClientConnected;
                    _horizonNetworkShare.Events.ClientDisconnected += Events_ClientDisconnected;
                    _horizonNetworkShare.Events.MessageReceived += Events_MessageReceived;
                    _horizonNetworkShare.Events.ExceptionEncountered += Events_ExceptionEncountered;
#pragma warning disable CS4014
                    _horizonNetworkShare.Start();
#pragma warning restore CS4014

                    try
                    {
                        _horizonDiscoveryService = new RegisterService();
                        _horizonDiscoveryService.Name = $"Horizon ({Environment.MachineName})";
                        _horizonDiscoveryService.RegType = "_horizon._tcp";
                        _horizonDiscoveryService.ReplyDomain = "local.";
                        _horizonDiscoveryService.UPort = freePort;
                        _horizonDiscoveryService.Register();
                    }
                    catch (Exception exception)
                    {
                        Log.Error(exception, "Service discovery protocol could not be stared.");
                    }

                    _configuration.NetworkSharing = true;
                    break;
                case CheckState.Unchecked:
                    if (_horizonNetworkShare != null)
                    {
                        _horizonNetworkShare.Events.ClientConnected -= Events_ClientConnected;
                        _horizonNetworkShare.Events.ClientDisconnected -= Events_ClientDisconnected;
                        _horizonNetworkShare.Events.MessageReceived -= Events_MessageReceived;
                        _horizonNetworkShare.Events.ExceptionEncountered -= Events_ExceptionEncountered;

                        _horizonNetworkShare.Dispose();
                        _horizonNetworkShare = null;
                    }

                    if (_horizonDiscoveryService != null)
                    {
                        _horizonDiscoveryService.Dispose();
                        _horizonDiscoveryService = null;

                    }

                    _configuration.NetworkSharing = false;
                    break;
            }
        }

        private void Events_ExceptionEncountered(object sender, ExceptionEventArgs e)
        {
            Log.Error(e.Exception,$"Client threw exception.");
        }

        private async void Events_MessageReceived(object sender, MessageReceivedEventArgs e)
        {
            Log.Information($"Client {e.Client?.IpPort} sent {e.Data?.Length} bytes via network sharing.");

            if (e.Data?.Length == 0)
            {
                return;
            }

            try
            {
                //var payload = Encoding.UTF8.GetString();

                using var memoryStream = new MemoryStream(e.Data);
                using var streamReader = new StreamReader(memoryStream);
                using var jsonTextReader = new JsonTextReader(streamReader);
                var completeSnapshot = _jsonSerializer.Deserialize<Snapshot>(jsonTextReader);

                await _snapshotDatabase.ApplySnapshotAsync(completeSnapshot, _cancellationToken);

                Log.Information($"Stored {completeSnapshot.Name} from {e.Client?.IpPort}");
            }
            catch (Exception exception)
            {
                Log.Error(exception, $"Failed to process network share from {e.Client?.IpPort}.");
            }
        }

        private void Events_ClientDisconnected(object sender, WatsonTcp.DisconnectionEventArgs e)
        {
            Log.Information($"Client {e.Client?.IpPort} disconnected from network sharing.");
        }

        private void Events_ClientConnected(object sender, WatsonTcp.ConnectionEventArgs e)
        {
            Log.Information($"Client {e.Client?.IpPort} connected to network sharing.");
        }

        private void WindowToolStripMenuItem_Click(object sender, EventArgs e)
        {
            windowToolStripMenuItem.Checked = true;
            screenToolStripMenuItem.Checked = false;

            _configuration.CaptureMode = CaptureMode.Window;
        }

        private void ScreenToolStripMenuItem_Click(object sender, EventArgs e)
        {
            screenToolStripMenuItem.Checked = true;
            windowToolStripMenuItem.Checked = false;

            _configuration.CaptureMode = CaptureMode.Screen;
        }

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

            _logViewForm = new LogViewForm(this, _memorySink, _cancellationToken);
            _logViewForm.Closing += LogViewFormClosing;
            _logViewForm.Show();
        }

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

            _logViewForm.Closing -= LogViewFormClosing;
            _logViewForm.Close();
            _logViewForm = null;
        }

        private async void SnapshotDatabase_SnapshotCreateAsync(object sender, SnapshotCreateEventArgs e)
        {
            switch (e)
            {
                case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
                    var snapshot = snapshotCreateSuccessEventArgs.Snapshot;
                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Snapshot Created", $"Took a snapshot of {snapshot.Path}.",
                            5000);
                    }

                    if(_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Create))
                    {
                        await SendGotifyNotification("Snapshot Created", $"Took a snapshot of {snapshot.Name}.");
                    }

                    Log.Information($"Took a snapshot of {snapshot.Path}.");

                    break;
                case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Snapshot Failed",
                            $"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.", 5000);
                    }

                    Log.Information($"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.");

                    break;
            }
        }
        private async void _snapshotDatabase_SnapshotDataUpdate(object sender, SnapshotDataUpdateEventArgs e)
        {
            switch(e)
            {
                case SnapshotDataUpdateSuccessEventArgs snapshotDataUpdateSuccessEventArgs:
                    if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Update))
                    {
                        await SendGotifyNotification("Snapshot Updated", $"Snapshot data updated {snapshotDataUpdateSuccessEventArgs.NewHash} from {snapshotDataUpdateSuccessEventArgs.OldHash}.");
                    }
                    break;
            }
        }

        private async void _snapshotDatabase_SnapshotNoteUpdate(object sender, SnapshotNoteUpdateEventArgs e)
        {
            switch(e)
            {
                case SnapshotNoteUpdateSuccessEventArgs snapshotNoteUpdateSuccessEventArgs:
                    if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Update))
                    {
                        await SendGotifyNotification("Snapshot Updated", $"Snapshot note updated for {snapshotNoteUpdateSuccessEventArgs.Hash}.");
                    }
                    break;
            }

        }

        private async void SnapshotDatabase_SnapshotTransferReceived(object sender, SnapshotCreateEventArgs e)
        {
            switch (e)
            {
                case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
                    var snapshot = snapshotCreateSuccessEventArgs.Snapshot;
                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Snapshot Transfer Success", $"A snapshot has been transferred {snapshot.Path}.",
                            5000);
                    }

                    if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Transfer))
                    {
                        await SendGotifyNotification("Snapshot Transerred", $"A snapshot has been transferred {snapshot.Name}.");
                    }

                    Log.Information($"A snapshot transfer succeeded {snapshot.Path}.");

                    break;
                case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Snapshot Transfer Failure",
                            $"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.", 5000);
                    }

                    Log.Information($"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.");

                    break;
            }
        }

        private async void SnapshotDatabase_SnapshotRevert(object sender, SnapshotRevertEventArgs e)
        {
            switch (e)
            {
                case SnapshotRevertSuccessEventArgs snapshotRevertSuccessEventArgs:
                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Revert Succeeded", $"File {snapshotRevertSuccessEventArgs.Name} reverted.", 5000);
                    }

                    if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Revert))
                    {
                        await SendGotifyNotification("Snapshot Reverted", $"Reverted a snapshot of {snapshotRevertSuccessEventArgs.Name}.");
                    }


                    Log.Information($"File {snapshotRevertSuccessEventArgs.Name} reverted.");

                    break;
                case SnapshotRevertFailureEventArgs snapshotRevertFailureEventArgs:
                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Revert Failed", $"Reverting file {snapshotRevertFailureEventArgs.Name} failed.",
                            5000);
                    }

                    Log.Information($"Reverting file {snapshotRevertFailureEventArgs.Name} failed.");

                    break;
            }
        }

        private void Folder_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.OldItems != null)
            {
                foreach (var item in e.OldItems.OfType<Folder>())
                {
                    RemoveWatcher(item.Path);
                }
            }

            if (e.NewItems != null)
            {
                foreach (var item in e.NewItems.OfType<Folder>())
                {
                    // If the folder is not enabled then do not add watchers for the path.
                    if (!item.Enable)
                    {
                        continue;
                    }

                    if (Directory.Exists(item.Path))
                    {
                        AddWatcher(item.Path, item.Recursive);
                    }
                }
            }

            _trackedFoldersChangedContinuation.Schedule(TimeSpan.FromSeconds(1), SaveFolders, _cancellationToken);
        }

        private async void MainForm_Load(object sender, EventArgs e)
        {
            // Start application update.
            var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
            var icon = Icon.ExtractAssociatedIcon(manifestModuleName);

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

            await _sparkle.StartLoop(true, true);

            // attempt an upgrade
#pragma warning disable CS4014
            //PerformUpgrade();
#pragma warning restore CS4014

            // Set form properties.
            toolStripMenuItem3.DropDown.Closing += toolStropMenuItem3DropDown_Closing;
            eventsToolStripMenuItem.DropDown.Closing += eventsToolStripMenuItem_Closing;

            // Load configuration.
            _configuration = await LoadConfiguration();
            _configuration.PropertyChanged += _configuration_PropertyChanged;

            launchOnBootToolStripMenuItem.Checked = _configuration.LaunchOnBoot;
            atomicOperationsToolStripMenuItem.Checked = _configuration.AtomicOperations;
            autoNotesToolStripMenuItem.Checked = _configuration.AutoNotes;
            gotifyToolStripMenuItem.Checked = _configuration.EnableGotify;
            gotifyToolStripTextBox.Text = _configuration.GotifyURL;
            enableToolStripMenuItem.Checked = _configuration.Enabled;
            showBalloonTooltipsToolStripMenuItem.Checked = _configuration.Enabled;
            windowToolStripMenuItem.Checked = _configuration.CaptureMode == CaptureMode.Window;
            screenToolStripMenuItem.Checked = _configuration.CaptureMode == CaptureMode.Screen;
            networkSharingToolStripMenuItem.Checked = _configuration.NetworkSharing;

            _searchEngine = new SearchEngine(_configuration, _cancellationToken);

            foreach (var item in attributesToolStripMenuItem.DropDownItems.OfType<ToolStripMenuItem>())
            {
                var text = item.Text;

                if (Enum.TryParse<NotifyFilters>(text, out var notifyFilter))
                {
                    item.Checked = _configuration.NotifyFilters.HasFlag(notifyFilter);
                }
            }

            // Load all tracked folders.
            try
            {
                var folders = await LoadFolders();
                foreach (var folder in folders.Folder)
                {
                    _trackedFolders.Folder.Add(folder);
                }

                ToggleWatchers();

                return;
            }
            catch (FileNotFoundException)
            {
                ToggleWatchers();

                return;
            }
            catch(Exception exception)
            {
                Log.Error(exception, "Error loading tracked folders.");
            }

            if (System.Windows.Forms.MessageBox.Show("Tracked folders could not be loaded, should they be deleted?", "Question", MessageBoxButtons.YesNo) == DialogResult.No)
            {
                return;
            }

            ToggleWatchers();
        }

        private void _configuration_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            _changedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1), SaveConfiguration, _cancellationToken);
        }

        private void ShowBalloonTooltipsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
        {
            _configuration.ShowBalloonTooltips = ((ToolStripMenuItem)sender).Checked;
        }

        private void LaunchOnBootToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
        {
            _configuration.LaunchOnBoot = ((ToolStripMenuItem)sender).Checked;

            Miscellaneous.LaunchOnBootSet(_configuration.LaunchOnBoot);
        }

        private void AtomicOperationsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
        {
            _configuration.AtomicOperations = ((ToolStripMenuItem)sender).Checked;
        }

        private void TrashDatabaseToolStripMenuItem_Click(object sender, EventArgs e)
        {
            try
            {
                File.Delete(Constants.DatabaseFilePath);

                if (_configuration.ShowBalloonTooltips)
                {
                    ShowBalloon("Database Deleted", $"Database file {Constants.DatabaseFilePath} has been deleted.",
                        5000);
                }

                Log.Information($"Database file {Constants.DatabaseFilePath} has been deleted.");
            }
            catch (Exception exception)
            {
                if (_configuration.ShowBalloonTooltips)
                {
                    ShowBalloon("Could not Delete Database",
                        $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}",
                        5000);
                }

                Log.Information(
                    $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}");
            }
        }

        private void EnableToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
        {
            _configuration.Enabled = enableToolStripMenuItem.Checked;

            ToggleWatchers();
        }

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

            _snapshotManagerForm = new SnapshotManagerForm(_configuration, _trackedFolders, _fileSystemWatchers, _snapshotDatabase, _searchEngine, _cancellationToken);
            _snapshotManagerForm.Closing += SnapshotManagerFormClosing;
            _snapshotManagerForm.Show();
        }

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

            _snapshotManagerForm.Closing -= SnapshotManagerFormClosing;
            _snapshotManagerForm.Close();
            _snapshotManagerForm = null;
        }

        private void FileSystemWatcher_Deleted(object sender, FileSystemEventArgs e)
        {
            Log.Information($"File deleted {e.Name}.");

            //ProcessFilesystemWatcherEvent(e.FullPath);
        }

        private void FileSystemWatcher_Created(object sender, FileSystemEventArgs e)
        {
            Log.Information($"File created {e.Name}.");

            ProcessFilesystemWatcherEvent(e.FullPath);
        }

        private void FileSystemWatcher_Renamed(object sender, RenamedEventArgs e)
        {
            Log.Information($"File renamed from {e.OldName} to {e.Name}.");

            ProcessFilesystemWatcherEvent(e.FullPath);
        }

        private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            Log.Information($"File changed {e.Name}.");

            ProcessFilesystemWatcherEvent(e.FullPath);
        }

        private void ProcessFilesystemWatcherEvent(string path)
        {
            // Ignore directories.
            if (Directory.Exists(path))
            {
                return;
            }
#pragma warning disable CS4014
            Task.Run(async () =>
#pragma warning restore CS4014
            {
                await _changedFilesLock.WaitAsync(_cancellationToken);
                try
                {
                    var delay = global::TrackedFolders.Constants.Delay;
                    var color = Color.Empty;

                    if (_trackedFolders.TryGet(path, out var folder))
                    {
                        delay = folder.Delay;
                        color = folder.Color;
                    }

                    if (_changedFiles.Contains(path))
                    {
                        _changedFilesContinuation.Schedule(delay,
                            async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
                        return;
                    }

                    _changedFiles.Add(path);

                    _changedFilesContinuation.Schedule(delay,
                        async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
                }
                catch (Exception exception)
                {
                    Log.Error(exception, "Could not process changed files.");
                }
                finally
                {
                    _changedFilesLock.Release();
                }
            }, CancellationToken.None);
        }

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

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

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

            _aboutForm.Dispose();
            _aboutForm = null;
        }

        private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

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

        private void NotifyIcon1_Click(object sender, EventArgs e)
        {
            if (e is MouseEventArgs mouseEventArgs && mouseEventArgs.Button == MouseButtons.Left)
            {
            }
        }

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

            _manageFoldersForm = new ManageFoldersForm(_configuration, _trackedFolders, _snapshotDatabase, _searchEngine, _cancellationToken);
            _manageFoldersForm.Closing += ManageFoldersForm_Closing;
            _manageFoldersForm.Show();
        }

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

            _manageFoldersForm.Closing -= ManageFoldersForm_Closing;
            _manageFoldersForm.Close();
            _manageFoldersForm = null;
        }

        #endregion

        #region Public Methods

        public void ShowBalloon(string title, string text, int time)
        {
            notifyIcon1.BalloonTipTitle = title;
            notifyIcon1.BalloonTipText = text;
            notifyIcon1.ShowBalloonTip(time);
        }

        public async Task SaveConfiguration()
        {
            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("Serialized configuration.");
                    break;
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
                    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, "Failed to load configuration.");
                    return new Configuration.Configuration();
                default:
                    return new Configuration.Configuration();
            }
        }

        public static async Task<TrackedFolders.TrackedFolders> LoadFolders()
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
            {
                Directory.CreateDirectory(Constants.UserApplicationDirectory);
            }

            var deserializationResult =
                await Serialization.Deserialize<TrackedFolders.TrackedFolders>(Constants.FoldersFile,
                    Constants.TrackedFoldersNamespace, Constants.TrackedFoldersXsd, CancellationToken.None);

            switch (deserializationResult)
            {
                case SerializationSuccess<TrackedFolders.TrackedFolders> serializationSuccess:
                    return serializationSuccess.Result;
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception, "Failed to load tracked folders");
                    throw serializationFailure.Exception;
            }

            return null;
        }

        public async Task SaveFolders()
        {
            if (!Directory.Exists(Constants.UserApplicationDirectory))
            {
                Directory.CreateDirectory(Constants.UserApplicationDirectory);
            }

            switch (await Serialization.Serialize(_trackedFolders, Constants.FoldersFile, "TrackedFolders",
                        "<!ATTLIST TrackedFolders xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
                        CancellationToken.None))
            {
                case SerializationSuccess<TrackedFolders.TrackedFolders> _:
                    Log.Information("Serialized tracked folders.");
                    break;
                case SerializationFailure serializationFailure:
                    Log.Warning(serializationFailure.Exception.Message, "Failed to serialize tracked folders.");
                    break;
            }
        }

        #endregion

        #region Private Methods

        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;
                        }
                    }

                    // Only offer an update nag screen if the version is one month old since skipping an update.
                    if (DateTime.Now.Subtract(latestVersion.PublicationDate).TotalDays < 30)
                    {
                        return;
                    }

                    var decision = System.Windows.Forms.MessageBox.Show(
                        "Update available but it has been previously skipped and a month has passed since. Should the update proceed now?",
                        Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.YesNo,
                        MessageBoxIcon.Asterisk,
                        MessageBoxDefaultButton.Button1, System.Windows.Forms.MessageBoxOptions.DefaultDesktopOnly, false);

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

                    goto default;
                case UpdateStatus.UpdateNotAvailable:
                    System.Windows.Forms.MessageBox.Show("No updates available at this time.",
                        Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.OK,
                        MessageBoxIcon.Asterisk,
                        MessageBoxDefaultButton.Button1, System.Windows.Forms.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 void RemoveWatcher(string folder)
        {
            var count = _fileSystemWatchers.Count;
            while (_fileSystemWatchers.TryDequeue(out var fileSystemWatcher))
            {
                if(--count == 0)
                {
                    break;
                }

                if (fileSystemWatcher.Path.IsPathEqual(folder) ||
                    fileSystemWatcher.Path.IsSubPathOf(folder))
                {
                    continue;
                }

                _fileSystemWatchers.Enqueue(fileSystemWatcher);
            }

        }

        private void AddWatcher(string folder, bool recursive)
        {
            var fileSystemWatcher = new FileSystemWatcher
            {
                IncludeSubdirectories = recursive,
                NotifyFilter = _fileSystemWatchersNotifyFilters,
                Path = folder,
                EnableRaisingEvents = true,
                InternalBufferSize = 65536
            };

            fileSystemWatcher.Changed += FileSystemWatcher_Changed;
            fileSystemWatcher.Renamed += FileSystemWatcher_Renamed;
            fileSystemWatcher.Created += FileSystemWatcher_Created;
            fileSystemWatcher.Deleted += FileSystemWatcher_Deleted;

            _fileSystemWatchers.Enqueue(fileSystemWatcher);
        }

        private void ToggleWatchers()
        {
            switch (_configuration.Enabled)
            {
                case true:
                    foreach (var watcher in _fileSystemWatchers)
                    {
                        watcher.EnableRaisingEvents = true;
                    }

                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Watching", "Watching folders...", 5000);
                    }

                    Log.Information("Watching folders.");

                    break;
                default:
                    foreach (var watcher in _fileSystemWatchers)
                    {
                        watcher.EnableRaisingEvents = false;
                    }

                    if (_configuration.ShowBalloonTooltips)
                    {
                        ShowBalloon("Not Watching", "Folders are not being watched.", 5000);
                    }

                    Log.Information("Folders are not being watched.");

                    break;
            }
        }

        private async Task TakeSnapshots(Color color, CancellationToken cancellationToken)
        {

            var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions() {CancellationToken = cancellationToken});
            var actionBlock = new ActionBlock<string>(async path =>
            {
                // In case files have vanished strictly due to the time specified by the tracked folders delay.
                if (!File.Exists(path))
                {
                    Log.Warning($"File vanished after tracked folder delay: {path}");

                    return;
                }

                try
                {
                    var fileName = Path.GetFileName(path);
                    var screenCapture = ScreenCapture.Capture((Utilities.CaptureMode)_configuration.CaptureMode);

                    var terms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

                    if (_configuration.AutoNotes)
                    {
                        await foreach (var term in Extensions.RecognizeStrings(screenCapture, cancellationToken))
                        {
                            terms.Add(term);
                        }
                    }

                    var note = string.Join(" ", terms);

                    // async, decompose, branch
                    if (await _snapshotDatabase.CreateSnapshotAsync(fileName, path, screenCapture, color, note, cancellationToken) is Snapshot snapshot)
                    {
                        await _searchEngine.Index(snapshot, cancellationToken);
                    }
                }
                catch (SQLiteException exception)
                {
                    if (exception.ResultCode == SQLiteErrorCode.Constraint)
                    {
                        Log.Information(exception, "Snapshot already exists.");
                    }
                }
                catch (Exception exception)
                {
                    Log.Error(exception, $"Could not take snapshot of file {path}");
                }
            });

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

            await _changedFilesLock.WaitAsync(_cancellationToken);
            try
            {
                foreach (var path in _changedFiles)
                {
                    await bufferBlock.SendAsync(path, cancellationToken);
                }
                bufferBlock.Complete();
                await bufferBlock.Completion;
            }
            catch (Exception exception)
            {
                Log.Error(exception, "Could not take snapshots.");
            }
            finally
            {
                _changedFiles.Clear();
                _changedFilesLock.Release();
            }
        }

        private async Task SendGotifyNotification(string v1, string v2)
        {
            if (!Uri.TryCreate(_configuration.GotifyURL, UriKind.RelativeOrAbsolute, out var uri))
            {
                Log.Warning($"Invalid Gotify URL provided.");
                return;
            }
            var gotifyMessageSending = new GotifyMessageOutgoing()
            {
                Title = v1,
                Message = v2
            };

            var payload = JsonConvert.SerializeObject(gotifyMessageSending);
            using var stringContent = new StringContent(payload, Encoding.UTF8, "application/json");
            using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
            httpRequestMessage.Content = stringContent;
            var response = await _httpClient.SendAsync(httpRequestMessage, _cancellationToken);
            var responseContent = await response.Content.ReadAsStringAsync();
            var gotifyReply = JsonConvert.DeserializeObject<GotifyMessageIncoming>(responseContent);
            if (gotifyReply?.AppId == null)
            {
                Log.Error($"Failed Sending notification.");
            }
        }

        #endregion


    }
}

Generated by GNU Enscript 1.6.5.90.