using ShellProgressBar;
using System.Text.Json;
using View_by_Distance.Distance.Models.Stateless;
using View_by_Distance.FaceRecognitionDotNet;
using View_by_Distance.Shared.Models;

namespace View_by_Distance.Distance.Models;

public partial class E_Distance
{

    private readonly List<string> _Moved;
    private readonly List<double?> _Debug;
    private readonly List<string> _Renamed;
    private readonly Serilog.ILogger? _Log;
    private readonly int _FaceConfidencePercent;
    private readonly bool _DistanceRenameToMatch;
    private readonly double[] _RangeFaceConfidence;
    private readonly bool _DistanceMoveUnableToMatch;
    private readonly List<string> _AllMappedFaceFiles;
    private readonly List<string> _AllMappedFaceFileNames;
    private readonly List<string> _DuplicateMappedFaceFiles;

    public E_Distance(bool distanceMoveUnableToMatch, bool distanceRenameToMatch, int faceConfidencePercent, double[] rangeFaceConfidence)
    {
        _Debug = new();
        _Moved = new();
        _Renamed = new();
        _AllMappedFaceFiles = new();
        _AllMappedFaceFileNames = new();
        _DuplicateMappedFaceFiles = new();
        _RangeFaceConfidence = rangeFaceConfidence;
        _Log = Serilog.Log.ForContext<E_Distance>();
        _DistanceRenameToMatch = distanceRenameToMatch;
        _FaceConfidencePercent = faceConfidencePercent;
        _DistanceMoveUnableToMatch = distanceMoveUnableToMatch;
    }

    private static void MoveUnableToMatch(string file)
    {
        string checkFile = string.Concat(file, ".unk");
        if (File.Exists(file) && !File.Exists(checkFile))
            File.Move(file, checkFile);
    }

    private FaceDistanceContainer[] GetFaceDistanceContainers(MappingFromItem mappingFromItem, List<Face> intersectFaces)
    {
        FaceDistanceContainer[] results;
        int confidencePercent;
        int normalizedRectangle;
        FaceDistance faceDistance;
        FaceDistanceContainer faceDistanceContainer;
        List<FaceDistanceContainer> collection = new();
        foreach (Face face in intersectFaces)
        {
            if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null)
                throw new NotSupportedException();
            confidencePercent = Shared.Models.Stateless.Methods.ILocation.GetConfidencePercent(_FaceConfidencePercent, _RangeFaceConfidence, face.Location.Confidence);
            normalizedRectangle = Shared.Models.Stateless.Methods.ILocation.GetNormalizedRectangle(face.Location, Shared.Models.Stateless.ILocation.Digits, face.OutputResolution);
            if (face.FaceDistance?.Encoding is not null && face.FaceDistance.Encoding is FaceRecognitionDotNet.FaceEncoding faceEncoding)
                faceDistance = new(confidencePercent, faceEncoding, mappingFromItem.Id, mappingFromItem.IsWrongYear, mappingFromItem.MinimumDateTime, normalizedRectangle);
            else
            {
                faceEncoding = FaceRecognition.LoadFaceEncoding(face.FaceEncoding.RawEncoding);
                faceDistance = new(confidencePercent, faceEncoding, mappingFromItem.Id, mappingFromItem.IsWrongYear, mappingFromItem.MinimumDateTime, normalizedRectangle);
                lock (intersectFaces)
                    face.SetFaceDistance(faceDistance);
            }
            faceDistanceContainer = new(face, faceDistance);
            collection.Add(faceDistanceContainer);
        }
        results = collection.ToArray();
        return results;
    }

    private static List<FaceDistance> GetFaceDistanceEncodings(FaceDistanceContainer[] faceDistanceContainers)
    {
        List<FaceDistance> faceDistanceEncodings = new();
        foreach (FaceDistanceContainer faceDistanceContainer in faceDistanceContainers)
        {
            if (faceDistanceContainer.FaceDistance.Encoding is null)
                continue;
            faceDistanceEncodings.Add(faceDistanceContainer.FaceDistance);
        }
        return faceDistanceEncodings;
    }

    private List<(Face Face, double? Length)> GetValues(MappingFromItem mappingFromItem, List<Face> intersectFaces, Shared.Models.FaceEncoding modelsFaceEncoding)
    {
        List<(Face Face, double? Length)> results = new();
        Face face;
        FaceDistance faceDistanceLength;
        FaceRecognitionDotNet.FaceEncoding faceRecognitionDotNetFaceEncoding = FaceRecognition.LoadFaceEncoding(modelsFaceEncoding.RawEncoding);
        FaceDistance faceDistanceEncoding = new(faceRecognitionDotNetFaceEncoding);
        FaceDistanceContainer[] faceDistanceContainers = GetFaceDistanceContainers(mappingFromItem, intersectFaces);
        int faceDistanceContainersLength = faceDistanceContainers.Length;
        if (faceDistanceContainersLength != intersectFaces.Count)
            throw new NotSupportedException();
        List<FaceDistance> faceDistanceEncodings = GetFaceDistanceEncodings(faceDistanceContainers);
        if (faceDistanceEncodings.Count != intersectFaces.Count)
            throw new NotSupportedException();
        List<FaceDistance> faceDistanceLengths = FaceRecognition.FaceDistances(faceDistanceEncodings, faceDistanceEncoding);
        if (faceDistanceLengths.Count != faceDistanceContainersLength)
            throw new NotSupportedException();
        for (int i = 0; i < intersectFaces.Count; i++)
        {
            face = intersectFaces[i];
            faceDistanceLength = faceDistanceLengths[i];
            if (faceDistanceLength.Length is null)
                throw new NotSupportedException();
            results.Add(new(face, faceDistanceLength.Length.Value));
        }
        return results;
    }

    private (Face, double?)[] GetClosestFaceByDistanceIgnoringTolerance(MappingFromItem mappingFromItem, List<Face> intersectFaces, Shared.Models.FaceEncoding modelsFaceEncoding)
    {
        (Face, double?)[] results;
        List<(Face Face, double? Length)> collection = GetValues(mappingFromItem, intersectFaces, modelsFaceEncoding);
        results = (from l in collection orderby l.Length select l).Take(1).ToArray();
        if (results.Any())
        {
            (Face _, double? length) = results.First();
            _Debug.Add(length);
        }
        return results;
    }

    private static List<(Face, double?)> GetMatchingFacesByFaceEncoding(Face[] filteredFaces, string? json)
    {
        List<(Face, double?)> results = new();
        string check;
        foreach (Face face in filteredFaces)
        {
            if (json is null || face.FaceEncoding is null)
                continue;
            if (!json.Contains(face.FaceEncoding.RawEncoding[0].ToString()))
                continue;
            check = JsonSerializer.Serialize(face.FaceEncoding);
            if (check != json)
                continue;
            results.Add(new(face, 0));
        }
        return results;
    }

    private static FileInfo? CheckFileThenGetFileInfo(string facesFileNameExtension, MappingFromItem mappingFromItem, string file, List<(Face, double?)> checkFaces)
    {
        FileInfo? result = null;
        string checkFile;
        string? mappedFaceDirectory;
        string deterministicHashCodeKey;
        foreach ((Face face, _) in checkFaces)
        {
            if (checkFaces.Count != 1)
                break;
            if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null)
                throw new NotSupportedException();
            mappedFaceDirectory = Path.GetDirectoryName(file);
            if (mappedFaceDirectory is null)
                throw new NotSupportedException();
            deterministicHashCodeKey = Shared.Models.Stateless.Methods.IMapping.GetDeterministicHashCodeKey(mappingFromItem.Id, face.Location, Shared.Models.Stateless.ILocation.Digits, face.OutputResolution);
            checkFile = Path.Combine(mappedFaceDirectory, $"{deterministicHashCodeKey}{mappingFromItem.ImageFileHolder.ExtensionLowered}{facesFileNameExtension}");
            if (checkFile == file)
                continue;
            result = new FileInfo(checkFile);
        }
        return result;
    }

    private void AppendMatchingDuplicates(string file, string[] matches)
    {
        string checkFile;
        FileInfo fileInfo = new(file);
        List<(long Length, string FullName)> collection = new();
        if (fileInfo.Exists)
            collection.Add(new(fileInfo.Length, fileInfo.FullName));
        lock (_DuplicateMappedFaceFiles)
            _DuplicateMappedFaceFiles.Add(file);
        foreach (string match in matches)
        {
            fileInfo = new(match);
            if (!fileInfo.Exists)
                continue;
            collection.Add(new(fileInfo.Length, fileInfo.FullName));
            break;
        }
        collection = collection.OrderBy(l => l.Length).ToList();
        for (int i = 0; i < collection.Count - 1; i++)
        {
            checkFile = string.Concat(collection[i].FullName, ".dup");
            if (File.Exists(checkFile))
                continue;
            File.Move(collection[i].FullName, checkFile);
        }
    }

    public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, string eDistanceContentDirectory, MappingFromItem mappingFromItem, List<Face> faces, List<LocationContainer<MetadataExtractor.Directory>> collection)
    {
        string? json;
        string fileName;
        string[] matches;
        FileInfo? fileInfo;
        List<Face> intersectFaces;
        List<(Face, double?)> checkFaces = new();
        Shared.Models.FaceEncoding? modelsFaceEncoding;
        Face[] filteredFaces = (from l in faces where l.FaceEncoding is not null && l.Location is not null && l.OutputResolution is not null select l).ToArray();
        if (filteredFaces.Length != faces.Count)
            checkFaces.Clear();
        foreach (LocationContainer<MetadataExtractor.Directory>? locationContainer in collection)
        {
            if (_Renamed.Contains(locationContainer.File))
                continue;
            fileName = Path.GetFileName(locationContainer.File);
            if (locationContainer.FromDistanceContent && _DuplicateMappedFaceFiles.Contains(fileName))
                continue;
            checkFaces.Clear();
            if (!locationContainer.Directories.Any())
            {
                if (locationContainer.FromDistanceContent)
                    throw new NullReferenceException(nameof(locationContainer.Directories));
                continue;
            }
            json = Metadata.Models.Stateless.Methods.IMetadata.GetFaceEncoding(locationContainer.Directories);
            if (json is null)
            {
                if (_DistanceMoveUnableToMatch)
                    MoveUnableToMatch(locationContainer.File);
                continue;
            }
            if (filteredFaces.Any())
                checkFaces.AddRange(GetMatchingFacesByFaceEncoding(filteredFaces, json));
            if (checkFaces.Count == 1)
                _Debug.Add(0);
            if (checkFaces.Count != 1 && !string.IsNullOrEmpty(json))
            {
                checkFaces.Clear();
                modelsFaceEncoding = JsonSerializer.Deserialize<Shared.Models.FaceEncoding>(json);
                if (modelsFaceEncoding is null)
                    throw new NotSupportedException();
                if (filteredFaces.Any())
                {
                    intersectFaces = Shared.Models.Stateless.Methods.ILocation.FilterByIntersect(filteredFaces, locationContainer.NormalizedRectangle);
                    if (intersectFaces.Any())
                        checkFaces.AddRange(GetClosestFaceByDistanceIgnoringTolerance(mappingFromItem, intersectFaces, modelsFaceEncoding));
                }
            }
            if (!checkFaces.Any())
            {
                if (_DistanceMoveUnableToMatch)
                    MoveUnableToMatch(locationContainer.File);
                continue;
            }
            if (checkFaces.Count != 1)
            {
                if (_DistanceMoveUnableToMatch)
                    MoveUnableToMatch(locationContainer.File);
                continue;
            }
            fileInfo = CheckFileThenGetFileInfo(facesFileNameExtension, mappingFromItem, locationContainer.File, checkFaces);
            if (fileInfo is not null)
            {
                if (_DistanceRenameToMatch && fileInfo is not null)
                {
                    if (fileInfo.Exists)
                        File.Delete(locationContainer.File);
                    else
                        File.Move(locationContainer.File, fileInfo.FullName);
                    File.WriteAllText($"{fileInfo.FullName}.old", $"{fileInfo.FullName}{Environment.NewLine}{locationContainer.File}");
                    _Renamed.Add(locationContainer.File);
                }
                continue;
            }
            if (_AllMappedFaceFileNames.Contains(fileName))
            {
                lock (_AllMappedFaceFiles)
                    matches = (from l in _AllMappedFaceFiles where l != locationContainer.File && Path.GetFileName(l) == fileName select l).ToArray();
                if (locationContainer.FromDistanceContent && matches.Any())
                    AppendMatchingDuplicates(locationContainer.File, matches);
            }
            if (!locationContainer.FromDistanceContent)
                continue;
            lock (_AllMappedFaceFiles)
                _AllMappedFaceFiles.Add(locationContainer.File);
            lock (_AllMappedFaceFileNames)
                _AllMappedFaceFileNames.Add(fileName);
        }
    }

    public void Clear()
    {
        if (_Log is null)
            throw new NullReferenceException(nameof(_Log));
        double?[] debug = (from l in _Debug where l is null or not 0 select l).ToArray();
        if (debug.Any())
        {
            string debugMessage = $"{_Debug.Count - debug.Length} - {debug.Min()} - {_Debug.Max()}";
            _Log.Info(debugMessage);
        }
        if (_DuplicateMappedFaceFiles.Any())
            _Log.Info($"Renamed {_DuplicateMappedFaceFiles.Count} to *.dup file(s)");
        if (_Moved.Any() || _Renamed.Any())
            throw new NotImplementedException("Restart!");
        _Debug.Clear();
        _Moved.Clear();
        _Renamed.Clear();
        _AllMappedFaceFiles.Clear();
        _AllMappedFaceFileNames.Clear();
        _DuplicateMappedFaceFiles.Clear();
    }

    public List<FaceDistanceContainer> GetMissingFaceDistanceContainer(int maxDegreeOfParallelism, long ticks, string dFacesCollectionDirectory, Dictionary<int, Dictionary<int, PersonContainer[]>> missingIdThenNormalizedRectangleToPersonContainers)
    {
        List<FaceDistanceContainer> results = new();
        string[] files;
        List<Face>? faces;
        int confidencePercent;
        int normalizedRectangle;
        bool? isWrongYear = null;
        FaceDistance faceDistance;
        List<(int id, string json)> collection = new();
        FaceDistanceContainer faceDistanceContainer;
        foreach (KeyValuePair<int, Dictionary<int, PersonContainer[]>> keyValuePair in missingIdThenNormalizedRectangleToPersonContainers)
        {
            files = Directory.GetFiles(dFacesCollectionDirectory, $"{keyValuePair.Key}*.json", SearchOption.TopDirectoryOnly);
            if (files.Length != 1)
                continue;
            collection.Add(new(keyValuePair.Key, Shared.Models.Stateless.Methods.IFace.GetJson(files[0])));
        }
        int totalSeconds = (int)Math.Floor(new TimeSpan(DateTime.Now.Ticks - ticks).TotalSeconds);
        ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
        string message = $") {collection.Count:000} Setting missing distance containers - {totalSeconds} total second(s)";
        ProgressBarOptions options = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true };
        using ProgressBar progressBar = new(collection.Count, message, options);
        _ = Parallel.For(0, collection.Count, parallelOptions, (i, state) =>
        {
            progressBar.Tick();
            int id = collection[i].id;
            string json = collection[i].json;
            faces = JsonSerializer.Deserialize<List<Face>>(json);
            if (faces is null)
                throw new NullReferenceException(nameof(faces));
            foreach (Face face in faces)
            {
                if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null)
                    continue;
                confidencePercent = Shared.Models.Stateless.Methods.ILocation.GetConfidencePercent(_FaceConfidencePercent, _RangeFaceConfidence, face.Location.Confidence);
                normalizedRectangle = Shared.Models.Stateless.Methods.ILocation.GetNormalizedRectangle(face.Location, Shared.Models.Stateless.ILocation.Digits, face.OutputResolution);
                if (face.FaceDistance?.Encoding is not null && face.FaceDistance.Encoding is FaceRecognitionDotNet.FaceEncoding faceEncoding)
                    faceDistance = new(confidencePercent, faceEncoding, id, isWrongYear, face.DateTime, normalizedRectangle);
                else
                {
                    faceEncoding = FaceRecognition.LoadFaceEncoding(face.FaceEncoding.RawEncoding);
                    faceDistance = new(confidencePercent, faceEncoding, id, isWrongYear, face.DateTime, normalizedRectangle);
                    face.SetFaceDistance(faceDistance);
                }
                faceDistanceContainer = new(face, faceDistance);
                lock (results)
                    results.Add(faceDistanceContainer);
            }
        });
        return results;
    }

}