using Quantum.Entities;
using Quantum.Server.Plugins;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Quantum.Server.Plugin.Example
{
    [PluginDetails("{EC86E82A-6359-4293-B3DC-20BBAA078E8F}", "List Values Mirror Plug-in", "Synchronizes each of the team's list properties with a directory on disk")]
    public class ListValuesMirrorPlugin : Plugins.Plugin
    {
        static readonly Regex changedEntryRelativePathPattern = new Regex(@"^(?<attributeId>{?[\da-f]{8}-?([\da-f]{4}-?){3}[\da-f]{12}}?)([/\\](?<textFileName>(Name|Notes|{?[\da-f]{8}-?([\da-f]{4}-?){3}[\da-f]{12}}?))\.txt)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        static async Task<(DirectoryInfo directory, Guid attributeId, FileInfo file, bool isAttributeName, bool isAttributeNotes, Guid enumerationValueId, string fileContents)> GetFileSystemEntryDetailsAsync(string teamDirectoryFullPath, string changedEntryFullPath)
        {
            DirectoryInfo directory = null;
            FileInfo file = null;
            Guid attributeId = Guid.Empty, enumerationValueId = Guid.Empty;
            bool isAttributeName = false, isAttributeNotes = false;
            string fileContents = null;
            if (changedEntryFullPath.Length > teamDirectoryFullPath.Length)
            {
                var match = changedEntryRelativePathPattern.Match(changedEntryFullPath.Substring(teamDirectoryFullPath.Length + 1));
                if (match.Success)
                {
                    var textFileNameGroup = match.Groups["textFileName"];
                    if (textFileNameGroup.Success)
                    {
                        file = new FileInfo(changedEntryFullPath);
                        attributeId = Guid.Parse(file.Directory.Name);
                        var fileName = textFileNameGroup.Value;
                        if (!(isAttributeName = fileName.Equals("Name", StringComparison.OrdinalIgnoreCase)) && !(isAttributeNotes = fileName.Equals("Notes", StringComparison.OrdinalIgnoreCase)))
                            enumerationValueId = Guid.Parse(fileName);
                        if (File.Exists(changedEntryFullPath))
                        {
                            try
                            {
                                using (var streamReader = new StreamReader(changedEntryFullPath))
                                    fileContents = await streamReader.ReadToEndAsync().ConfigureAwait(false);
                            }
                            catch (IOException)
                            {
                                // whoops, maybe we now don't have permission to read this?
                            }
                        }
                    }
                    else
                    {
                        directory = new DirectoryInfo(changedEntryFullPath);
                        attributeId = Guid.Parse(directory.Name);
                    }
                }
            }
            return (directory, attributeId, file, isAttributeName, isAttributeNotes, enumerationValueId, fileContents);
        }

        static bool IsFileSystemInfoKeyValid((FileSystemInfo fileSystemInfo, Guid key) tuple) => tuple.key != default;

        static async Task<(string name, string notes, IReadOnlyDictionary<Guid, AttributeCorrelation<EnumerationValue>> enumerationValues)> LoadAttributeAsync(Guid attributeId, DirectoryInfo directory)
        {
            string name = null, notes = null;
            var enumerationValues = new Dictionary<Guid, AttributeCorrelation<EnumerationValue>>();
            foreach (var file in directory.GetFiles("*.txt"))
            {
                if (file.Name.Equals("Name.txt", StringComparison.OrdinalIgnoreCase) && name != null)
                    using (var streamReader = new StreamReader(file.FullName))
                        name = await streamReader.ReadToEndAsync().ConfigureAwait(false);
                else if (file.Name.Equals("Notes.txt", StringComparison.OrdinalIgnoreCase) && notes != null)
                    using (var streamReader = new StreamReader(file.FullName))
                        notes = await streamReader.ReadToEndAsync().ConfigureAwait(false);
                else if (Guid.TryParse(file.Name.Substring(0, file.Name.Length - file.Extension.Length), out var enumerationValueId) && !enumerationValues.ContainsKey(enumerationValueId))
                    using (var streamReader = new StreamReader(file.FullName))
                        enumerationValues.Add(enumerationValueId, new AttributeCorrelation<EnumerationValue>(attributeId, new EnumerationValue(await streamReader.ReadToEndAsync().ConfigureAwait(false))));
            }
            return (name, notes, enumerationValues);
        }

        static (TKey key, TValue value) SelectKeyValueTuple<TKey, TValue>(KeyValuePair<TKey, TValue> keyValuePair) => (keyValuePair.Key, keyValuePair.Value);

        static IEnumerable<(Guid id, FileInfo file)> SelectManyFiles(Dictionary<Guid, FileInfo> dictionary) => dictionary.Select(SelectKeyValueTuple);

        static TKey SelectTupleKey<TKey, TValue>((TKey key, TValue value) tuple) => tuple.key;

        static TValue SelectTupleValue<TKey, TValue>((TKey key, TValue value) tuple) => tuple.value;

        static Guid SelectFileSystemInfoKey(FileSystemInfo fileSystemInfo) => Guid.Parse(fileSystemInfo.Name.Substring(0, fileSystemInfo.Name.Length - fileSystemInfo.Extension.Length));

        static Guid SelectFileSystemInfoKey((FileSystemInfo fileSystemInfo, Guid key) tuple) => tuple.key;

        static FileInfo SelectFileSystemInfoValueAsFileInfo((FileSystemInfo fileSystemInfo, Guid key) tuple) => (FileInfo)tuple.fileSystemInfo;

        static (FileSystemInfo fileSystemInfo, Guid key) SelectFileSystemInfoAndKey(FileSystemInfo fileSystemInfo) => (fileSystemInfo, Guid.TryParse(fileSystemInfo.Name.Substring(0, fileSystemInfo.Name.Length - fileSystemInfo.Extension.Length), out var key) ? key : default);

        FileSystemWatcher fileSystemWatcher;
        ConcurrentDictionary<(string fullPath, WatcherChangeTypes watcherChangeType), bool> ignoreFileSystemEvents = new ConcurrentDictionary<(string fullPath, WatcherChangeTypes watcherChangeType), bool>();
        DirectoryInfo pluginDirectory;
        DirectoryInfo teamDirectory;

        async void FileSystemWatcherEventHandler(object sender, FileSystemEventArgs e)
        {
            try
            {
                if (ignoreFileSystemEvents.TryRemove((e.FullPath, e.ChangeType), out _) || (Directory.Exists(e.FullPath) && e.ChangeType == WatcherChangeTypes.Changed))
                    return;
                if (e.FullPath.Length > teamDirectory.FullName.Length)
                {
                    var (targetDirectory, targetAttributeId, targetFile, targetIsAttributeName, targetIsAttributeNotes, targetEnumerationValueId, targetFileContents) = await GetFileSystemEntryDetailsAsync(teamDirectory.FullName, e.FullPath).ConfigureAwait(false);
                    var frame = await SnapshotAsync().ConfigureAwait(false);
                    var isTargetAttributeExistent = frame.Attributes.TryGetValue(targetAttributeId, out var existingTargetAttribute);
                    var isTargetEnumerationValueExistent = frame.EnumerationValues.TryGetValue(targetEnumerationValueId, out var existingTargetEnumerationValue);
                    ChangeSet changeSet = null;
                    switch (e.ChangeType)
                    {
                        case WatcherChangeTypes.Changed when targetIsAttributeName && targetFileContents != existingTargetAttribute?.Name:
                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(targetFileContents, existingTargetAttribute?.Notes, true) } });
                            break;
                        case WatcherChangeTypes.Changed when targetIsAttributeNotes && targetFileContents != existingTargetAttribute?.Notes:
                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(existingTargetAttribute?.Name, targetFileContents, true) } });
                            break;
                        case WatcherChangeTypes.Changed when isTargetEnumerationValueExistent && targetFileContents != existingTargetEnumerationValue?.CorrelatedEntity.Name:
                            changeSet = new ChangeSet(createOrUpdateEnumerationValues: new Dictionary<Guid, AttributeCorrelation<EnumerationValue>> { { targetEnumerationValueId, new AttributeCorrelation<EnumerationValue>(targetAttributeId, new EnumerationValue(targetFileContents)) } });
                            break;
                        case WatcherChangeTypes.Created when targetIsAttributeName:
                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(targetFileContents, existingTargetAttribute?.Notes, true) } });
                            break;
                        case WatcherChangeTypes.Created when targetIsAttributeNotes:
                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(existingTargetAttribute?.Name, targetFileContents, true) } });
                            break;
                        case WatcherChangeTypes.Created when targetEnumerationValueId != Guid.Empty:
                            changeSet = new ChangeSet(createOrUpdateEnumerationValues: new Dictionary<Guid, AttributeCorrelation<EnumerationValue>> { { targetEnumerationValueId, new AttributeCorrelation<EnumerationValue>(targetAttributeId, new EnumerationValue(targetFileContents)) } });
                            break;
                        case WatcherChangeTypes.Created when targetAttributeId != Guid.Empty:
                            {
                                var (name, notes, enumerationValues) = await LoadAttributeAsync(targetAttributeId, targetDirectory).ConfigureAwait(false);
                                changeSet = new ChangeSet
                                (
                                    createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(name, notes, true) } },
                                    createOrUpdateEnumerationValues: enumerationValues
                                );
                            }
                            break;
                        case WatcherChangeTypes.Deleted when targetIsAttributeName && existingTargetAttribute.Name != null:
                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(null, existingTargetAttribute?.Notes, true) } });
                            break;
                        case WatcherChangeTypes.Deleted when targetIsAttributeNotes && existingTargetAttribute.Notes != null:
                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(existingTargetAttribute?.Name, null, true) } });
                            break;
                        case WatcherChangeTypes.Deleted when isTargetEnumerationValueExistent:
                            changeSet = new ChangeSet(deleteEnumerationValues: new Guid[] { targetEnumerationValueId });
                            break;
                        case WatcherChangeTypes.Deleted when isTargetAttributeExistent:
                            changeSet = new ChangeSet(deleteAttributes: new Guid[] { targetAttributeId });
                            break;
                        case WatcherChangeTypes.Renamed:
                            if (e is RenamedEventArgs re)
                            {
                                var (originalDirectory, originalAttributeId, originalFile, originalIsAttributeName, originalIsAttributeNotes, originalEnumerationValueId, _) = await GetFileSystemEntryDetailsAsync(teamDirectory.FullName, re.OldFullPath).ConfigureAwait(false);
                                var isOriginalAttributeExistent = frame.Attributes.TryGetValue(originalAttributeId, out var existingOriginalAttribute);
                                var isOriginalEnumerationValueExistent = frame.EnumerationValues.TryGetValue(originalEnumerationValueId, out var existingOriginalEnumerationValue);
                                if (originalAttributeId != Guid.Empty)
                                {
                                    if (targetAttributeId == Guid.Empty || originalAttributeId != targetAttributeId)
                                    {
                                        if (originalIsAttributeName && existingOriginalAttribute.Name != null)
                                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(null, existingOriginalAttribute?.Notes, true) } });
                                        else if (originalIsAttributeNotes && existingOriginalAttribute.Notes != null)
                                            changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(existingOriginalAttribute?.Name, null, true) } });
                                        else if (isOriginalEnumerationValueExistent)
                                            changeSet = new ChangeSet(deleteEnumerationValues: new Guid[] { originalEnumerationValueId });
                                        else if (isOriginalAttributeExistent)
                                            changeSet = new ChangeSet(deleteAttributes: new Guid[] { originalAttributeId });
                                    }
                                    else if (originalIsAttributeName && targetIsAttributeNotes)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(null, targetFileContents, true) } });
                                    else if (originalIsAttributeName && targetEnumerationValueId != Guid.Empty)
                                        changeSet = new ChangeSet
                                        (
                                            createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(null, existingOriginalAttribute?.Notes, true) } },
                                            createOrUpdateEnumerationValues: new Dictionary<Guid, AttributeCorrelation<EnumerationValue>> { { targetEnumerationValueId, new AttributeCorrelation<EnumerationValue>(originalAttributeId, new EnumerationValue(targetFileContents)) } }
                                        );
                                    else if (originalIsAttributeNotes && targetIsAttributeName)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(targetFileContents, null, true) } });
                                    else if (originalIsAttributeNotes && targetEnumerationValueId != Guid.Empty)
                                        changeSet = new ChangeSet
                                        (
                                            createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(existingOriginalAttribute?.Name, null, true) } },
                                            createOrUpdateEnumerationValues: new Dictionary<Guid, AttributeCorrelation<EnumerationValue>> { { targetEnumerationValueId, new AttributeCorrelation<EnumerationValue>(originalAttributeId, new EnumerationValue(targetFileContents)) } }
                                        );
                                    else if (originalEnumerationValueId != Guid.Empty && targetIsAttributeName)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(targetFileContents, null, true) } });
                                    else if (originalEnumerationValueId != Guid.Empty && targetIsAttributeNotes)
                                        changeSet = new ChangeSet
                                        (
                                            createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(existingOriginalAttribute?.Name, targetFileContents, true) } },
                                            deleteEnumerationValues: new Guid[] { targetEnumerationValueId }
                                        );
                                    else if (originalIsAttributeName)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(null, existingOriginalAttribute?.Notes, true) } });
                                    else if (originalIsAttributeNotes)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { originalAttributeId, new Quantum.Entities.Attribute(existingOriginalAttribute?.Name, null, true) } });
                                    else if (originalEnumerationValueId != Guid.Empty)
                                        changeSet = new ChangeSet(deleteEnumerationValues: new Guid[] { originalEnumerationValueId });
                                    else if (targetIsAttributeName)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(targetFileContents, existingTargetAttribute?.Notes, true) } });
                                    else if (targetIsAttributeNotes)
                                        changeSet = new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(existingTargetAttribute?.Name, targetFileContents, true) } });
                                    else if (targetEnumerationValueId != Guid.Empty)
                                        changeSet = new ChangeSet(createOrUpdateEnumerationValues: new Dictionary<Guid, AttributeCorrelation<EnumerationValue>> { { targetEnumerationValueId, new AttributeCorrelation<EnumerationValue>(targetAttributeId, new EnumerationValue(targetFileContents)) } });
                                }
                                if (targetAttributeId != Guid.Empty && originalAttributeId != targetAttributeId)
                                {
                                    if (targetIsAttributeName)
                                        changeSet = ChangeSet.Merge(changeSet, new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(targetFileContents, existingTargetAttribute?.Notes, true) } }));
                                    else if (targetIsAttributeNotes)
                                        changeSet = ChangeSet.Merge(changeSet, new ChangeSet(createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(existingTargetAttribute?.Name, targetFileContents, true) } }));
                                    else if (targetEnumerationValueId != Guid.Empty)
                                        changeSet = ChangeSet.Merge(changeSet, new ChangeSet(createOrUpdateEnumerationValues: new Dictionary<Guid, AttributeCorrelation<EnumerationValue>> { { targetEnumerationValueId, new AttributeCorrelation<EnumerationValue>(targetAttributeId, new EnumerationValue(targetFileContents)) } }));
                                    else if (targetAttributeId != Guid.Empty)
                                    {
                                        var (name, notes, enumerationValues) = await LoadAttributeAsync(targetAttributeId, targetDirectory).ConfigureAwait(false);
                                        changeSet = ChangeSet.Merge(changeSet, new ChangeSet
                                        (
                                            createOrUpdateAttributes: new Dictionary<Guid, Quantum.Entities.Attribute> { { targetAttributeId, new Quantum.Entities.Attribute(name, notes, true) } },
                                            createOrUpdateEnumerationValues: enumerationValues
                                        ));
                                    }
                                }
                                break;
                            }
                            throw new NotSupportedException();
                    }
                    if (changeSet != null)
                        await ApplySignedChangesAsync(SignChanges(changeSet)).ConfigureAwait(false);
                }
            }
            catch
            {
            }
        }

        public override async Task<ChangeSet> OnChangeSetAppliedAsync(IndexedFrame originalState, ChangeSet changesApplied, ChangeSet inScopeChangesApplied)
        {
            var attributeDirectoriesById = teamDirectory.GetDirectories().ToDictionary(SelectFileSystemInfoKey);
            pluginDirectory.Refresh();
            if (!pluginDirectory.Exists)
                pluginDirectory.Create();
            teamDirectory.Refresh();
            if (!teamDirectory.Exists)
                teamDirectory.Create();
            foreach (var kv in changesApplied.CreateOrUpdateAttributes ?? Enumerable.Empty<KeyValuePair<Guid, Quantum.Entities.Attribute>>())
            {
                var attributeId = kv.Key;
                var attribute = kv.Value;
                if (!attributeDirectoriesById.TryGetValue(attributeId, out var attributeDirectory))
                {
                    if (originalState.Attributes.TryGetValue(attributeId, out var updatedAttribute) && (attribute.Name == updatedAttribute.Name || attribute.Name == null) && (attribute.Notes == updatedAttribute.Notes || attribute.Notes == null))
                        continue;
                    attributeDirectory = new DirectoryInfo(Path.Combine(teamDirectory.FullName, attributeId.ToString("N")));
                    attributeDirectoriesById.Add(attributeId, attributeDirectory);
                    if (!attributeDirectory.Exists)
                    {
                        ignoreFileSystemEvents.TryAdd((attributeDirectory.FullName, WatcherChangeTypes.Created), true);
                        attributeDirectory.Create();
                    }
                }
                var nameFilePath = Path.Combine(attributeDirectory.FullName, "Name.txt");
                var nameFileExists = File.Exists(nameFilePath);
                if (attribute.Name == null && nameFileExists)
                {
                    ignoreFileSystemEvents.TryAdd((nameFilePath, WatcherChangeTypes.Deleted), true);
                    File.Delete(nameFilePath);
                }
                else if (attribute.Name != null)
                {
                    if (!nameFileExists)
                        ignoreFileSystemEvents.TryAdd((nameFilePath, WatcherChangeTypes.Created), true);
                    ignoreFileSystemEvents.TryAdd((nameFilePath, WatcherChangeTypes.Changed), true);
                    using (var writer = new StreamWriter(Path.Combine(attributeDirectory.FullName, "Name.txt"), false))
                        await writer.WriteAsync(attribute.Name).ConfigureAwait(false);
                }
                var notesFilePath = Path.Combine(attributeDirectory.FullName, "Notes.txt");
                var notesFileExists = File.Exists(notesFilePath);
                if (attribute.Notes == null && notesFileExists)
                {
                    ignoreFileSystemEvents.TryAdd((notesFilePath, WatcherChangeTypes.Deleted), true);
                    File.Delete(notesFilePath);
                }
                else if (attribute.Notes != null)
                {
                    if (!notesFileExists)
                        ignoreFileSystemEvents.TryAdd((notesFilePath, WatcherChangeTypes.Created), true);
                    ignoreFileSystemEvents.TryAdd((notesFilePath, WatcherChangeTypes.Changed), true);
                    using (var writer = new StreamWriter(notesFilePath, false))
                        await writer.WriteAsync(attribute.Notes).ConfigureAwait(false);
                }
            }
            var enumerationValueFilesByIdByAttributeId = new Dictionary<Guid, Dictionary<Guid, FileInfo>>();
            foreach (var kv in changesApplied.CreateOrUpdateEnumerationValues ?? Enumerable.Empty<KeyValuePair<Guid, AttributeCorrelation<EnumerationValue>>>())
            {
                var enumerationValue = kv.Value;
                var attributeId = enumerationValue.AttributeId;
                var attributeDirectory = attributeDirectoriesById[attributeId];
                if (!enumerationValueFilesByIdByAttributeId.TryGetValue(attributeId, out var enumerationValueFilesById))
                {
                    enumerationValueFilesById = attributeDirectory.GetFiles().Select(SelectFileSystemInfoAndKey).Where(IsFileSystemInfoKeyValid).ToDictionary(SelectFileSystemInfoKey, SelectFileSystemInfoValueAsFileInfo);
                    enumerationValueFilesByIdByAttributeId.Add(attributeId, enumerationValueFilesById);
                }
                var enumerationValueId = kv.Key;
                if (!enumerationValueFilesById.TryGetValue(enumerationValueId, out var enumerationValueFile))
                {
                    enumerationValueFile = new FileInfo(Path.Combine(attributeDirectory.FullName, $"{enumerationValueId:N}.txt"));
                    enumerationValueFilesById.Add(enumerationValueId, enumerationValueFile);
                }
                if (!enumerationValueFile.Exists)
                    ignoreFileSystemEvents.TryAdd((enumerationValueFile.FullName, WatcherChangeTypes.Created), true);
                ignoreFileSystemEvents.TryAdd((enumerationValueFile.FullName, WatcherChangeTypes.Changed), true);
                using (var writer = new StreamWriter(enumerationValueFile.FullName, false))
                    await writer.WriteAsync(enumerationValue.CorrelatedEntity.Name).ConfigureAwait(false);
            }
            if (changesApplied.DeleteEnumerationValues?.Any() ?? false)
            {
                foreach (var kv in attributeDirectoriesById)
                {
                    var attributeId = kv.Key;
                    if (!enumerationValueFilesByIdByAttributeId.TryGetValue(attributeId, out var enumerationValueFilesById))
                    {
                        enumerationValueFilesById = kv.Value.GetFiles().Select(SelectFileSystemInfoAndKey).Where(IsFileSystemInfoKeyValid).ToDictionary(SelectFileSystemInfoKey, SelectFileSystemInfoValueAsFileInfo);
                        enumerationValueFilesByIdByAttributeId.Add(attributeId, enumerationValueFilesById);
                    }
                }
                var allEnumerationValueFileById = enumerationValueFilesByIdByAttributeId.Values.SelectMany(SelectManyFiles).ToDictionary(SelectTupleKey, SelectTupleValue);
                foreach (var enumerationValueId in changesApplied.DeleteEnumerationValues ?? Enumerable.Empty<Guid>())
                    if (allEnumerationValueFileById.TryGetValue(enumerationValueId, out var enumerationValueFile))
                    {
                        ignoreFileSystemEvents.TryAdd((enumerationValueFile.FullName, WatcherChangeTypes.Deleted), true);
                        enumerationValueFile.Delete();
                    }
            }
            foreach (var attributeId in changesApplied.DeleteAttributes ?? Enumerable.Empty<Guid>())
                if (attributeDirectoriesById.TryGetValue(attributeId, out var attributeDirectory))
                {
                    ignoreFileSystemEvents.TryAdd((attributeDirectory.FullName, WatcherChangeTypes.Deleted), true);
                    attributeDirectory.Delete(true);
                }
            return await base.OnChangeSetAppliedAsync(originalState, changesApplied, inScopeChangesApplied).ConfigureAwait(false);
        }

        public override async Task OnLoadedAsync()
        {
            pluginDirectory = new DirectoryInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "GSS Example List Values Mirror Plug-in"));
            if (!pluginDirectory.Exists)
                pluginDirectory.Create();
            teamDirectory = new DirectoryInfo(Path.Combine(pluginDirectory.FullName, TeamId.ToString("N")));
            if (!teamDirectory.Exists)
                teamDirectory.Create();
            foreach (var entry in teamDirectory.GetFileSystemInfos())
            {
                if (entry is DirectoryInfo dirEntry)
                    dirEntry.Delete(true);
                else
                    entry.Delete();
            }
            var frame = await SnapshotAsync().ConfigureAwait(false);
            foreach (var kv in frame.Attributes)
            {
                var attribute = kv.Value;
                if (attribute.IsEnumeration)
                {
                    var attributeId = kv.Key;
                    var attributeDirectory = new DirectoryInfo(Path.Combine(teamDirectory.FullName, attributeId.ToString("N")));
                    attributeDirectory.Create();
                    if (attribute.Name != null)
                        using (var writer = new StreamWriter(Path.Combine(attributeDirectory.FullName, "Name.txt")))
                            await writer.WriteAsync(attribute.Name).ConfigureAwait(false);
                    if (attribute.Notes != null)
                        using (var writer = new StreamWriter(Path.Combine(attributeDirectory.FullName, "Notes.txt")))
                            await writer.WriteAsync(attribute.Notes).ConfigureAwait(false);
                    foreach (var enumerationValueId in frame.EnumerationValueIdsByAttributeId[attributeId])
                        using (var writer = new StreamWriter(Path.Combine(attributeDirectory.FullName, $"{enumerationValueId:N}.txt")))
                            await writer.WriteAsync(frame.EnumerationValues[enumerationValueId].CorrelatedEntity.Name).ConfigureAwait(false);
                }
            }
            fileSystemWatcher = new FileSystemWatcher(teamDirectory.FullName)
            {
                IncludeSubdirectories = true,
                NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size
            };
            fileSystemWatcher.Changed += FileSystemWatcherEventHandler;
            fileSystemWatcher.Created += FileSystemWatcherEventHandler;
            fileSystemWatcher.Deleted += FileSystemWatcherEventHandler;
            fileSystemWatcher.Renamed += FileSystemWatcherEventHandler;
            fileSystemWatcher.EnableRaisingEvents = true;
            await base.OnLoadedAsync().ConfigureAwait(false);
        }

        public override Task OnUnloadingAsync()
        {
            fileSystemWatcher.EnableRaisingEvents = false;
            fileSystemWatcher.Changed -= FileSystemWatcherEventHandler;
            fileSystemWatcher.Created -= FileSystemWatcherEventHandler;
            fileSystemWatcher.Deleted -= FileSystemWatcherEventHandler;
            fileSystemWatcher.Renamed -= FileSystemWatcherEventHandler;
            fileSystemWatcher?.Dispose();
            return base.OnUnloadingAsync();
        }
    }
}
