using ShellProgressBar;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using View_by_Distance.Property.Models.Stateless;
using View_by_Distance.Shared.Models;
using View_by_Distance.Shared.Models.Stateless;

namespace View_by_Distance.Property.Models;

public class A_Property
{

    protected readonly List<string> _ExceptionsDirectories;

    public bool Reverse { get; }
    public List<string> AngleBracketCollection { get; }
    public List<string> ExceptionsDirectories => _ExceptionsDirectories;

    private readonly Serilog.ILogger? _Log;
    private readonly string _OutputExtension;
    private readonly string[] _VerifyToSeason;
    private readonly int _MaxDegreeOfParallelism;
    private readonly ASCIIEncoding _ASCIIEncoding;
    private readonly Configuration _Configuration;
    private readonly JsonSerializerOptions _WriteIndentedJsonSerializerOptions;

    public A_Property(int maxDegreeOfParallelism, Configuration configuration, string outputExtension, bool reverse)
    {
        Reverse = reverse;
        _Configuration = configuration;
        _ExceptionsDirectories = new();
        _OutputExtension = outputExtension;
        _ASCIIEncoding = new ASCIIEncoding();
        AngleBracketCollection = new List<string>();
        _Log = Serilog.Log.ForContext<A_Property>();
        _MaxDegreeOfParallelism = maxDegreeOfParallelism;
        _WriteIndentedJsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true };
        if (configuration.VerifyToSeason is null || !configuration.VerifyToSeason.Any())
            throw new Exception();
        _VerifyToSeason = configuration.VerifyToSeason.Select(l => Path.Combine(configuration.RootDirectory, l)).ToArray();
    }

    public override string ToString()
    {
        string result = JsonSerializer.Serialize(this, new JsonSerializerOptions() { WriteIndented = true });
        return result;
    }

    private long LogDelta(long ticks, string? methodName)
    {
        long result;
        if (_Log is null)
            throw new NullReferenceException(nameof(_Log));
        double delta = new TimeSpan(DateTime.Now.Ticks - ticks).TotalMilliseconds;
        _Log.Debug($"{methodName} took {Math.Floor(delta)} millisecond(s)");
        result = DateTime.Now.Ticks;
        return result;
    }

#pragma warning disable CA1416

    private Shared.Models.Property GetImageProperty(FileHolder fileHolder, Shared.Models.Property? property, bool populateId, bool isIgnoreExtension, bool isValidImageFormatExtension, bool isValidMetadataExtensions, int? id)
    {
        Shared.Models.Property result;
        if (_Log is null)
            throw new NullReferenceException(nameof(_Log));
        long ticks;
        byte[] bytes;
        string value;
        long fileLength;
        int encodingHash;
        int? width = null;
        int? height = null;
        string dateTimeFormat;
        DateTime checkDateTime;
        DateTime? dateTime = null;
        PropertyItem? propertyItem;
        string make = string.Empty;
        string model = string.Empty;
        DateTime? gpsDateStamp = null;
        DateTime? dateTimeOriginal = null;
        string orientation = string.Empty;
        DateTime? dateTimeDigitized = null;
        DateTime? dateTimeFromName = Shared.Models.Stateless.Methods.IProperty.GetDateTimeFromName(fileHolder);
        if (!isValidImageFormatExtension && isValidMetadataExtensions && fileHolder.Exists)
        {
            dateTimeFormat = "ddd MMM dd HH:mm:ss yyyy";
            List<DateTime> dateTimes = Shared.Models.Stateless.Methods.IProperty.GetMetadataDateTimesByPattern(dateTimeFormat, fileHolder);
            if (dateTimes.Any())
                dateTimeOriginal = dateTimes.Min();
        }
        else if (!isIgnoreExtension && isValidImageFormatExtension && fileHolder.Exists)
        {
            try
            {
                using Image image = Image.FromFile(fileHolder.FullName);
                if (populateId && id is null)
                {
                    using Bitmap bitmap = new(image);
                    string angleBracket = AngleBracketCollection[0];
                    Rectangle rectangle = new(0, 0, image.Width, image.Height);
                    BitmapData bitmapData = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat);
                    IntPtr intPtr = bitmapData.Scan0;
                    int length = bitmapData.Stride * bitmap.Height;
                    bytes = new byte[length];
                    Marshal.Copy(intPtr, bytes, 0, length);
                    bitmap.UnlockBits(bitmapData);
                    if (id is null)
                    {
                        ticks = DateTime.Now.Ticks;
                        id = Shared.Models.Stateless.Methods.IProperty.GetDeterministicHashCode(bytes);
                        if (_MaxDegreeOfParallelism < 2)
                            ticks = LogDelta(ticks, nameof(Shared.Models.Stateless.Methods.IProperty.GetDeterministicHashCode));
                    }
                    if (_Configuration.WriteBitmapDataBytes)
                    {
                        FileInfo contentFileInfo = new(Path.Combine(angleBracket.Replace("<>", "()"), fileHolder.Name));
                        File.WriteAllBytes(Path.ChangeExtension(contentFileInfo.FullName, string.Empty), bytes);
                    }
                    ticks = DateTime.Now.Ticks;
                    string encoding = Encoding.Default.GetString(bytes);
                    if (_MaxDegreeOfParallelism < 2)
                        ticks = LogDelta(ticks, nameof(Encoding.Default.GetString));
                    encodingHash = Shared.Models.Stateless.Methods.IProperty.GetDeterministicHashCode(encoding);
                    if (_MaxDegreeOfParallelism < 2)
                        ticks = LogDelta(ticks, nameof(Shared.Models.Stateless.Methods.IProperty.GetDeterministicHashCode));
                }
                width = image.Width;
                height = image.Height;
                dateTimeFormat = Shared.Models.Stateless.Methods.IProperty.DateTimeFormat();
                if (image.PropertyIdList.Contains((int)IExif.Tags.DateTime))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.DateTime);
                    if (propertyItem?.Value is not null)
                    {
                        value = _ASCIIEncoding.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
                        if (value.Length > dateTimeFormat.Length)
                            value = value[..dateTimeFormat.Length];
                        if (value.Length == dateTimeFormat.Length && DateTime.TryParseExact(value, dateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out checkDateTime))
                            dateTime = checkDateTime;
                    }
                }
                if (image.PropertyIdList.Contains((int)IExif.Tags.DateTimeDigitized))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.DateTimeDigitized);
                    if (propertyItem?.Value is not null)
                    {
                        value = _ASCIIEncoding.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
                        if (value.Length > dateTimeFormat.Length)
                            value = value[..dateTimeFormat.Length];
                        if (value.Length == dateTimeFormat.Length && DateTime.TryParseExact(value, dateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out checkDateTime))
                            dateTimeDigitized = checkDateTime;
                    }
                }
                if (image.PropertyIdList.Contains((int)IExif.Tags.DateTimeOriginal))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.DateTimeOriginal);
                    if (propertyItem?.Value is not null)
                    {
                        value = _ASCIIEncoding.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
                        if (value.Length > dateTimeFormat.Length)
                            value = value[..dateTimeFormat.Length];
                        if (value.Length == dateTimeFormat.Length && DateTime.TryParseExact(value, dateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out checkDateTime))
                            dateTimeOriginal = checkDateTime;
                    }
                }
                if (image.PropertyIdList.Contains((int)IExif.Tags.GPSDateStamp))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.GPSDateStamp);
                    if (propertyItem?.Value is not null)
                    {
                        value = _ASCIIEncoding.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
                        if (value.Length > dateTimeFormat.Length)
                            value = value[..dateTimeFormat.Length];
                        if (value.Length == dateTimeFormat.Length && DateTime.TryParseExact(value, dateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out checkDateTime))
                            gpsDateStamp = checkDateTime;
                    }
                }
                if (image.PropertyIdList.Contains((int)IExif.Tags.Make))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.Make);
                    if (propertyItem?.Value is not null)
                    {
                        value = _ASCIIEncoding.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
                        make = value;
                    }
                }
                if (image.PropertyIdList.Contains((int)IExif.Tags.Model))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.Model);
                    if (propertyItem?.Value is not null)
                    {
                        value = _ASCIIEncoding.GetString(propertyItem.Value, 0, propertyItem.Len - 1);
                        model = value;
                    }
                }
                if (image.PropertyIdList.Contains((int)IExif.Tags.Orientation))
                {
                    propertyItem = image.GetPropertyItem((int)IExif.Tags.Orientation);
                    if (propertyItem?.Value is not null)
                    {
                        value = BitConverter.ToInt16(propertyItem.Value, 0).ToString();
                        orientation = value;
                    }
                }
            }
            catch (Exception)
            {
                _Log.Info(string.Concat(new StackFrame().GetMethod()?.Name, " <", fileHolder.Name, ">"));
            }
        }
        else
            dateTimeOriginal = null;
        if (fileHolder.Length is null)
            fileLength = 0;
        else
            fileLength = fileHolder.Length.Value;
        if (fileHolder.CreationTime is null && property?.CreationTime is null)
            throw new NullReferenceException(nameof(fileHolder.CreationTime));
        if (fileHolder.LastWriteTime is null && property?.LastWriteTime is null)
            throw new NullReferenceException(nameof(fileHolder.LastWriteTime));
        if (fileHolder.CreationTime is not null && fileHolder.LastWriteTime is not null)
            result = new(fileHolder.CreationTime.Value, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, fileHolder.LastWriteTime.Value, make, model, orientation, width);
        else if (property is not null)
            result = new(property.CreationTime, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, property.LastWriteTime, make, model, orientation, width);
        else
            throw new NullReferenceException(nameof(property));
        return result;
    }

#pragma warning restore CA1416

    private Shared.Models.Property GetPropertyOfPrivate(Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions)
    {
        Shared.Models.Property? result;
        string json;
        int? id = null;
        bool hasWrongYearProperty = false;
        string[] changesFrom = Array.Empty<string>();
        string angleBracket = AngleBracketCollection[0];
        bool populateId = _Configuration.PopulatePropertyId;
        string without = Path.Combine(angleBracket.Replace("<>", "{}"), $"{item.ImageFileHolder.NameWithoutExtension}.json");
        FileInfo fileInfo = new(Path.Combine(angleBracket.Replace("<>", "{}"), $"{item.ImageFileHolder.NameWithoutExtension}{item.ImageFileHolder.ExtensionLowered}.json"));
        if (item.ValidImageFormatExtension && File.Exists(without))
        {
            File.Move(without, fileInfo.FullName);
            fileInfo.Refresh();
        }
        List<DateTime> dateTimes = (from l in sourceDirectoryFileTuples where changesFrom.Contains(l.Item1) select l.Item2).ToList();
        if (_Configuration.ForcePropertyLastWriteTimeToCreationTime && !fileInfo.Exists && File.Exists(Path.ChangeExtension(fileInfo.FullName, ".delete")))
        {
            File.Move(Path.ChangeExtension(fileInfo.FullName, ".delete"), fileInfo.FullName);
            fileInfo.Refresh();
        }
        if (_Configuration.ForcePropertyLastWriteTimeToCreationTime && fileInfo.Exists && fileInfo.LastWriteTime != fileInfo.CreationTime)
        {
            File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime);
            fileInfo.Refresh();
        }
        if (_Configuration.PropertiesChangedForProperty)
            result = null;
        else if (!fileInfo.Exists)
            result = null;
        else if (!fileInfo.FullName.EndsWith(".json") && !fileInfo.FullName.EndsWith(".old"))
            throw new ArgumentException("must be a *.json file");
        else if (dateTimes.Any() && dateTimes.Max() > fileInfo.LastWriteTime)
            result = null;
        else
        {
            json = File.ReadAllText(fileInfo.FullName);
            try
            {
                if (item.Property is not null)
                    result = item.Property;
                else
                    result = JsonSerializer.Deserialize<Shared.Models.Property>(json);
                if (result is not null && json.Contains("WrongYear"))
                {
                    id = result.Id;
                    hasWrongYearProperty = true;
                    result = null;
                }
                if (!isIgnoreExtension && item.ValidImageFormatExtension && ((populateId && result?.Id is null) || result?.Width is null || result.Height is null))
                {
                    id = result?.Id;
                    result = null;
                }
                if (!isIgnoreExtension && item.ValidImageFormatExtension && populateId && result is not null)
                {
                    id = result.Id;
                    result = null;
                }
                if (!isIgnoreExtension && item.ValidImageFormatExtension && populateId && result is not null && result.LastWriteTime != item.ImageFileHolder.LastWriteTime)
                {
                    id = null;
                    result = null;
                }
                if (!isIgnoreExtension && item.ValidImageFormatExtension && result?.Width is not null && result.Height is not null && result.Width.Value == result.Height.Value && item.ImageFileHolder.Exists)
                {
                    id = result.Id;
                    result = null;
                    if (result?.Width is not null && result.Height is not null && result.Width.Value != result.Height.Value)
                        throw new Exception("Was square!");
                }
                if (!isIgnoreExtension && item.ValidImageFormatExtension && result is not null && result.FileSize != fileInfo.Length)
                {
                    id = result.Id;
                    result = null;
                }
                // if (filteredSourceDirectoryFileFileInfo.CreationTime != result.CreationTime || filteredSourceDirectoryFileFileInfo.LastWriteTime != result?.LastWriteTime)
                // {
                //     id = null;
                //     property = ;
                // }
                if (result is not null)
                    sourceDirectoryFileTuples.Add(new Tuple<string, DateTime>(nameof(A_Property), fileInfo.LastWriteTime));
            }
            catch (Exception)
            {
                result = null;
                parseExceptions.Add(nameof(A_Property));
            }
        }
        if (result is null)
        {
            id ??= item.ImageFileHolder.Id;
            result = GetImageProperty(item.ImageFileHolder, result, populateId, isIgnoreExtension, item.ValidImageFormatExtension, isValidMetadataExtensions, id);
            json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
            if (populateId && Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: true, compareBeforeWrite: true))
            {
                if (!_Configuration.ForcePropertyLastWriteTimeToCreationTime)
                    sourceDirectoryFileTuples.Add(new Tuple<string, DateTime>(nameof(A_Property), DateTime.Now));
                else
                {
                    File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime);
                    fileInfo.Refresh();
                    sourceDirectoryFileTuples.Add(new Tuple<string, DateTime>(nameof(A_Property), fileInfo.CreationTime));
                }
            }
        }
        else if (hasWrongYearProperty)
        {
            json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
            if (Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: true, compareBeforeWrite: true))
            {
                File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime);
                fileInfo.Refresh();
                sourceDirectoryFileTuples.Add(new Tuple<string, DateTime>(nameof(A_Property), fileInfo.CreationTime));
            }
        }
        return result;
    }

    private bool AnyFilesMoved(string sourceDirectory, List<Item> items)
    {
        bool result = false;
        if (_Log is null)
            throw new NullReferenceException(nameof(_Log));
        int season;
        string[] matches;
        string deleteFile;
        bool? isWrongYear;
        string seasonName;
        DateTime dateTime;
        string destinationFile;
        DateTime minimumDateTime;
        string destinationDirectory;
        string[] sourceDirectorySegments;
        DateTime directoryMaximumOfMinimumDateTime = DateTime.MinValue;
        foreach (Item item in items)
        {
            if (!item.ValidImageFormatExtension || item.Property is null || !item.ImageFileHolder.Exists)
                continue;
            minimumDateTime = Shared.Models.Stateless.Methods.IProperty.GetMinimumDateTime(item.Property);
            if (minimumDateTime > directoryMaximumOfMinimumDateTime)
                directoryMaximumOfMinimumDateTime = minimumDateTime;
            if (minimumDateTime != item.ImageFileHolder.CreationTime)
            {
                (isWrongYear, matches) = item.Property.IsWrongYear(item.ImageFileHolder, minimumDateTime);
                if (isWrongYear is null || !isWrongYear.Value)
                    dateTime = minimumDateTime;
                else
                {
                    if (!matches.Any())
                        continue;
                    if (!DateTime.TryParseExact(matches[0], "yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime))
                        continue;
                }
                try
                { File.SetCreationTime(item.ImageFileHolder.FullName, dateTime); }
                catch (Exception)
                { }
            }
            if (!_VerifyToSeason.Contains(sourceDirectory))
                continue;
            if (!item.ImageFileHolder.FullName.Contains("zzz ") && !item.ImageFileHolder.FullName.Contains("Camera ") && item.Property.DateTimeOriginal.HasValue)
            {
                TimeSpan timeSpan = new(item.Property.DateTimeOriginal.Value.Ticks - item.Property.LastWriteTime.Ticks);
                if (timeSpan.TotalHours > 7.2f)
                {
                    _Log.Warning($"*** propertyHolder.FileInfo.FullName <{item.ImageFileHolder.FullName}>");
                    _Log.Warning($"*** DateTimeOriginal <{item.Property.DateTimeOriginal.Value}>");
                    _Log.Warning($"*** LastWriteTime <{item.Property.LastWriteTime}>");
                    _Log.Warning($"*** TotalHours <{timeSpan.TotalHours}>");
                }
            }
            sourceDirectorySegments = Path.GetFileName(sourceDirectory).Split(' ');
            (season, seasonName) = Shared.Models.Stateless.Methods.IProperty.GetSeason(minimumDateTime.DayOfYear);
            if (sourceDirectorySegments[0] == "zzz")
                destinationDirectory = Path.Combine(_Configuration.RootDirectory, $"zzz ={minimumDateTime:yyyy}.{season} {seasonName} {string.Join(' ', sourceDirectorySegments.Skip(3))}");
            else if (sourceDirectorySegments.Length > 2)
                destinationDirectory = Path.Combine(_Configuration.RootDirectory, $"={minimumDateTime:yyyy}.{season} {seasonName} {string.Join(' ', sourceDirectorySegments.Skip(2))}");
            else
                destinationDirectory = Path.Combine(_Configuration.RootDirectory, $"={minimumDateTime:yyyy}.{season} {seasonName}");
            if (destinationDirectory == sourceDirectory)
                continue;
            lock (item)
                item.SetMoved(true);
            if (!result)
                result = true;
            if (!Directory.Exists(destinationDirectory))
                _ = Directory.CreateDirectory(destinationDirectory);
            destinationFile = Path.Combine(destinationDirectory, item.ImageFileHolder.Name);
            if (File.Exists(destinationFile))
            {
                if (_OutputExtension is not ".jpg" and not ".jpeg")
                    throw new Exception();
                if (destinationFile.EndsWith(".jpg", ignoreCase: true, CultureInfo.CurrentCulture))
                    destinationFile = Path.Combine(destinationDirectory, Path.ChangeExtension(item.ImageFileHolder.Name, ".jpeg"));
                else if (destinationFile.EndsWith(".jpeg", ignoreCase: true, CultureInfo.CurrentCulture))
                    destinationFile = Path.Combine(destinationDirectory, Path.ChangeExtension(item.ImageFileHolder.Name, ".jpg"));
            }
            _Log.Information($"*** source <{item.ImageFileHolder.FullName}>");
            _Log.Information($"*** destination <{destinationFile}>");
            if (!File.Exists(destinationFile))
                File.Move(item.ImageFileHolder.FullName, destinationFile);
            else
            {
                deleteFile = Path.ChangeExtension(item.ImageFileHolder.FullName, ".delete");
                if (File.Exists(deleteFile))
                    File.Delete(deleteFile);
                File.Move(item.ImageFileHolder.FullName, destinationFile);
            }
        }
        if (directoryMaximumOfMinimumDateTime != DateTime.MinValue)
        {
            System.IO.DirectoryInfo directoryInfo = new(sourceDirectory);
            if (directoryInfo.LastWriteTime != directoryMaximumOfMinimumDateTime)
                Directory.SetLastWriteTime(sourceDirectory, directoryMaximumOfMinimumDateTime);
        }
        return result;
    }

    private void SavePropertyParallelForWork(string sourceDirectory, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<Tuple<string, DateTime>> sourceDirectoryChanges, Item item)
    {
        Shared.Models.Property property;
        List<string> parseExceptions = new();
        bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
        bool isIgnoreExtension = item.ValidImageFormatExtension && _Configuration.IgnoreExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
        string filteredSourceDirectoryFileExtensionLowered = Path.Combine(sourceDirectory, $"{item.ImageFileHolder.NameWithoutExtension}{item.ImageFileHolder.ExtensionLowered}");
        if (item.ValidImageFormatExtension && item.ImageFileHolder.FullName.Length == filteredSourceDirectoryFileExtensionLowered.Length && item.ImageFileHolder.FullName != filteredSourceDirectoryFileExtensionLowered)
            File.Move(item.ImageFileHolder.FullName, filteredSourceDirectoryFileExtensionLowered);
        if (item.FileSizeChanged is null || item.FileSizeChanged.Value || item.LastWriteTimeChanged is null || item.LastWriteTimeChanged.Value || item.Property is null)
        {
            property = GetPropertyOfPrivate(item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions);
            lock (sourceDirectoryChanges)
                sourceDirectoryChanges.Add(new Tuple<string, DateTime>(nameof(A_Property), DateTime.Now));
            lock (item)
                item.Update(property);
        }
    }

    private void SavePropertyParallelWork(List<Exception> exceptions, List<Tuple<string, DateTime>> sourceDirectoryChanges, Shared.Models.Container container, List<Item> items, string message)
    {
        List<Tuple<string, DateTime>> sourceDirectoryFileTuples = new();
        ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = _MaxDegreeOfParallelism };
        ProgressBarOptions options = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true };
        using ProgressBar progressBar = new(items.Count, message, options);
        _ = Parallel.For(0, items.Count, parallelOptions, (i, state) =>
           {
               try
               {
                   long ticks = DateTime.Now.Ticks;
                   DateTime dateTime = DateTime.Now;
                   List<Tuple<string, DateTime>> collection;
                   SavePropertyParallelForWork(container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]);
                   if (i == 0 || sourceDirectoryChanges.Any())
                       progressBar.Tick();
                   lock (sourceDirectoryFileTuples)
                       collection = (from l in sourceDirectoryFileTuples where l.Item2 > dateTime select l).ToList();
                   lock (sourceDirectoryChanges)
                       sourceDirectoryChanges.AddRange(collection);
               }
               catch (Exception ex)
               {
                   lock (exceptions)
                       exceptions.Add(ex);
               }
           });
    }

    public void SetAngleBracketCollection(string aResultsFullGroupDirectory, string sourceDirectory)
    {
        AngleBracketCollection.Clear();
        AngleBracketCollection.AddRange(IResult.GetDirectoryInfoCollection(_Configuration,
                                                                           sourceDirectory,
                                                                           aResultsFullGroupDirectory,
                                                                           contentDescription: string.Empty,
                                                                           singletonDescription: "Properties for each image",
                                                                           collectionDescription: string.Empty,
                                                                           converted: false));
    }

    public void SetAngleBracketCollection(string sourceDirectory)
    {
        AngleBracketCollection.Clear();
        string aResultsFullGroupDirectory = IResult.GetResultsFullGroupDirectory(_Configuration,
                                                                                 nameof(A_Property),
                                                                                 string.Empty,
                                                                                 includeResizeGroup: false,
                                                                                 includeModel: false,
                                                                                 includePredictorModel: false);
        SetAngleBracketCollection(aResultsFullGroupDirectory, sourceDirectory);
    }

    public void SavePropertyParallelWork(long ticks, Shared.Models.Container[] containers)
    {
        if (_Log is null)
            throw new NullReferenceException(nameof(_Log));
        string message;
        int totalSeconds;
        bool? anyFilesMoved;
        Shared.Models.Container container;
        List<Exception> exceptions = new();
        int containersCount = containers.Length;
        List<Tuple<string, DateTime>> sourceDirectoryChanges = new();
        string propertyRoot = IResult.GetResultsGroupDirectory(_Configuration, nameof(A_Property));
        for (int i = 0; i < containers.Length; i++)
        {
            container = containers[i];
            if (!container.Items.Any())
                continue;
            sourceDirectoryChanges.Clear();
            if (!container.Items.Any())
                continue;
            SetAngleBracketCollection(container.SourceDirectory);
            totalSeconds = (int)Math.Truncate(new TimeSpan(DateTime.Now.Ticks - ticks).TotalSeconds);
            message = $"{i + 1:000}.{container.G} / {containersCount:000}) {container.Items.Count:000} file(s) - {totalSeconds} total second(s) - {container.SourceDirectory}";
            SavePropertyParallelWork(exceptions, sourceDirectoryChanges, container, container.Items, message);
            foreach (Exception exception in exceptions)
                _Log.Error(string.Concat(container.SourceDirectory, Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace), exception);
            if (exceptions.Count == container.Items.Count)
                throw new Exception(string.Concat("All in [", container.SourceDirectory, "]failed!"));
            if (exceptions.Count != 0)
                _ExceptionsDirectories.Add(container.SourceDirectory);
            if (exceptions.Count != 0)
                anyFilesMoved = null;
            else
                anyFilesMoved = AnyFilesMoved(container.SourceDirectory, container.Items);
            if (Directory.GetFiles(propertyRoot, "*.txt", SearchOption.TopDirectoryOnly).Any())
            {
                for (int y = 0; y < int.MaxValue; y++)
                {
                    _Log.Information("Press \"Y\" key when ready to continue or close console");
                    if (System.Console.ReadKey().Key == ConsoleKey.Y)
                        break;
                }
                _Log.Information(". . .");
            }
        }
    }

    public Shared.Models.Property GetProperty(Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions)
    {
        Shared.Models.Property result;
        bool angleBracketCollectionAny = AngleBracketCollection.Any();
        if (!angleBracketCollectionAny)
        {
            if (item.ImageFileHolder.DirectoryName is null)
                throw new NullReferenceException(nameof(item.ImageFileHolder.DirectoryName));
            SetAngleBracketCollection(item.ImageFileHolder.DirectoryName);
        }
        bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
        bool isIgnoreExtension = item.ValidImageFormatExtension && _Configuration.IgnoreExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
        result = GetPropertyOfPrivate(item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions);
        if (!angleBracketCollectionAny)
            AngleBracketCollection.Clear();
        return result;
    }

    public (long Ticks, string FilteredSourceDirectoryFile, string PropertyDirectory, int PropertyId)[] GetPropertyIds(List<DirectoryInfo> groupCollection, bool saveToCollection)
    {
        List<(long Ticks, string FilteredSourceDirectoryFile, string PropertyDirectory, int PropertyId)> results = new();
        int level;
        string checkDirectory;
        List<string> directories;
        string propertyDirectory;
        Shared.Models.Property? property;
        string angleBracket = AngleBracketCollection[0];
        foreach (DirectoryInfo group in groupCollection)
        {
            SetAngleBracketCollection(group.SourceDirectory);
            if (string.IsNullOrEmpty(group.SourceDirectory))
                throw new Exception();
            if (!saveToCollection)
                propertyDirectory = angleBracket.Replace("<>", "()");
            else
            {
                (level, directories) = Shared.Models.Stateless.Methods.IPath.Get(_Configuration.RootDirectory, group.SourceDirectory);
                checkDirectory = Shared.Models.Stateless.Methods.IPath.GetDirectory(angleBracket, level, "[()]");
                propertyDirectory = Path.Combine(checkDirectory, string.Join(_Configuration.FileNameDirectorySeparator, directories));
            }
            if (!Directory.Exists(propertyDirectory))
                _ = Directory.CreateDirectory(propertyDirectory);
            for (int i = 0; i < group.SourceDirectoryFileHolderCollection.Length; i++)
            {
                property = group.PropertyCollection[i];
                if (property?.Id is null)
                    continue;
                results.Add(new(property.GetDateTimes().Min().Ticks, group.FilteredSourceDirectoryFiles[i], propertyDirectory, property.Id.Value));
            }
        }
        return results.OrderBy(l => l.Ticks).ToArray();
    }

}