using MetadataExtractor;
using MetadataExtractor.Formats.Exif;
using System.Collections.ObjectModel;
using System.Text.Json;
using static View_by_Distance.Metadata.Models.Stateless.Methods.IMetadata;

namespace View_by_Distance.Metadata.Models.Stateless.Methods;

internal partial class Metadata
{

    internal static Dictionary<string, MetadataExtractorDirectory> GetKeyValuePairs(IReadOnlyList<MetadataExtractor.Directory> directories)
    {
        Dictionary<string, MetadataExtractorDirectory> results = [];
        MetadataExtractorTag metadataExtractorTag;
        MetadataExtractorDirectory? metadataExtractorDirectory;
        Dictionary<int, MetadataExtractorTag> metadataExtractorTags;
        foreach (MetadataExtractor.Directory directory in directories)
        {
            metadataExtractorTags = [];
            if (results.TryGetValue(directory.Name, out metadataExtractorDirectory))
                continue;
            foreach (Tag tag in directory.Tags)
            {
                metadataExtractorTag = new(tag.Type, tag.Description, tag.HasName, tag.Name);
                metadataExtractorTags.Add(tag.Type, metadataExtractorTag);
            }
            metadataExtractorDirectory = new(directory.Name, directory.HasError, new(directory.Errors.ToArray()), new(metadataExtractorTags));
            results.Add(directory.Name, metadataExtractorDirectory);
        }
        return results;
    }

    internal static Dictionary<string, MetadataExtractorDirectory> Deserialize(string json)
    {
        Dictionary<string, MetadataExtractorDirectory> results = [];
        Record? record;
        MetadataExtractorDirectory metadataExtractorDirectory;
        Dictionary<string, JsonElement>? keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
        if (keyValuePairs is null)
            throw new NullReferenceException(nameof(keyValuePairs));
        foreach (KeyValuePair<string, JsonElement> keyValuePair in keyValuePairs)
        {
            record = JsonSerializer.Deserialize(keyValuePair.Value.ToString(), RecordSourceGenerationContext.Default.Record);
            if (record is null)
                throw new NullReferenceException(nameof(record));
            metadataExtractorDirectory = new(record.Name, record.HasError, new(record.Errors), new(record.Tags));
            results.Add(record.Name, metadataExtractorDirectory);
        }
        return results;
    }

    internal static string? GetFaceEncoding(IReadOnlyList<MetadataExtractor.Directory> directories)
    {
        string? result;
        List<string> results = [];
        const string comment = "Comment: ";
        foreach (MetadataExtractor.Directory directory in directories)
        {
            if (directory.Name != "PNG-tEXt")
                continue;
            foreach (Tag tag in directory.Tags)
            {
                if (tag.Name != "Textual Data" || string.IsNullOrEmpty(tag.Description))
                    continue;
                if (!tag.Description.StartsWith(comment))
                    continue;
                results.Add(tag.Description);
            }
        }
        result = results.Count != 0 ? results[0][comment.Length..] : null;
        return result;
    }

    internal static string? GetOutputResolution(IReadOnlyList<MetadataExtractor.Directory> directories)
    {
        string? result;
        List<string> results = [];
        const string artist = "Artist: ";
        foreach (MetadataExtractor.Directory directory in directories)
        {
            if (directory.Name != "PNG-tEXt")
                continue;
            foreach (Tag tag in directory.Tags)
            {
                if (tag.Name != "Textual Data" || string.IsNullOrEmpty(tag.Description))
                    continue;
                if (!tag.Description.StartsWith(artist))
                    continue;
                results.Add(tag.Description);
            }
        }
        result = results.Count != 0 ? results[0][artist.Length..] : null;
        return result;
    }

    internal static string? GetModel(IReadOnlyList<MetadataExtractor.Directory> directories)
    {
        string? result;
        ExifDirectoryBase? exifDirectoryBase = directories.OfType<ExifDirectoryBase>().FirstOrDefault();
        if (exifDirectoryBase is null)
            result = null;
        else
            result = exifDirectoryBase.GetString(ExifDirectoryBase.TagModel);
        return result;
    }

    internal static GeoLocation? GeoLocation(IReadOnlyList<MetadataExtractor.Directory> directories)
    {
        GeoLocation? result;
        GpsDirectory? gpsDirectory = directories.OfType<GpsDirectory>().FirstOrDefault();
        if (gpsDirectory is null)
            result = null;
        else
            result = gpsDirectory.GetGeoLocation();
        return result;
    }

    private static bool CoordinateValidatorValidate(double latitude, double longitude)
    {
        if (latitude is < (-90) or > 90)
            return false;
        if (longitude is < (-180) or > 180)
            return false;

        return true;
    }

    private static double GetRadius(DistanceUnit distanceUnit)
    {
        return distanceUnit switch
        {
            DistanceUnit.Kilometers => 6371.0, // EarthRadiusInKilometers;
            DistanceUnit.Meters => 6371000.0, // EarthRadiusInMeters;
            DistanceUnit.NauticalMiles => 3440.0, // EarthRadiusInNauticalMiles;
            DistanceUnit.Miles => 3959.0, // EarthRadiusInMiles;
            _ => throw new NotSupportedException()
        };
    }

    private static double ToRadian(double d) =>
        d * (Math.PI / 180);

    private static double DiffRadian(double val1, double val2) =>
        ToRadian(val2) - ToRadian(val1);

    internal static double GetDistance(double originLatitude, double originLongitude, double destinationLatitude, double destinationLongitude, int decimalPlaces = 1, DistanceUnit distanceUnit = DistanceUnit.Miles)
    {
        if (!CoordinateValidatorValidate(originLatitude, originLongitude))
            throw new ArgumentException("Invalid origin coordinates supplied.");
        if (!CoordinateValidatorValidate(destinationLatitude, destinationLongitude))
            throw new ArgumentException("Invalid destination coordinates supplied.");
        double radius = GetRadius(distanceUnit);
        return Math.Round(
                radius * 2 *
                Math.Asin(Math.Min(1,
                                   Math.Sqrt(
                                       Math.Pow(Math.Sin(DiffRadian(originLatitude, destinationLatitude) / 2.0), 2.0) +
                                        Math.Cos(ToRadian(originLatitude)) * Math.Cos(ToRadian(destinationLatitude)) *
                                        Math.Pow(Math.Sin(DiffRadian(originLongitude, destinationLongitude) / 2.0),
                                                 2.0)))), decimalPlaces);
    }

    private static double ParseValueFromDmsString(string value)
    {
        double result;
        if (string.IsNullOrEmpty(value))
            return double.MinValue;

        double secondsValue;
        string[] degrees = value.Split('°');
        if (degrees.Length != 2)
            return double.MinValue;
        if (!double.TryParse(degrees[0], out double degreesValue))
            return double.MinValue;

        string[] minutes = degrees[1].Split('\'');
        if (minutes.Length != 2)
            return double.MinValue;
        if (!double.TryParse(minutes[0], out double minutesValue))
            return double.MinValue;

        string[] seconds = minutes[1].Split('"');
        if (seconds.Length != 2)
            secondsValue = 0;
        else
        {
            if (!double.TryParse(seconds[0], out secondsValue))
                return double.MinValue;
        }
        result = Math.Abs(degreesValue) + (minutesValue / 60) + (secondsValue / 3600);

        if (degreesValue < 0)
            result *= -1;

        return result;
    }

    internal static GeoLocation? GeoLocation(ReadOnlyDictionary<string, MetadataExtractorDirectory> metadataExtractorDirectories)
    {
        GeoLocation? result;
        if (!metadataExtractorDirectories.TryGetValue("GPS", out MetadataExtractorDirectory? metadataExtractorDirectory))
            result = null;
        else
        {
            MetadataExtractorTag? metadataExtractorTag;
            if (!metadataExtractorDirectory.Tags.TryGetValue((int)Shared.Models.Stateless.IExif.Tags.GPSLatitude, out metadataExtractorTag) || string.IsNullOrEmpty(metadataExtractorTag.Description))
                result = null;
            else
            {
                string latitudeDMS = metadataExtractorTag.Description;
                double latitude = ParseValueFromDmsString(latitudeDMS);
                if (!metadataExtractorDirectory.Tags.TryGetValue((int)Shared.Models.Stateless.IExif.Tags.GPSLongitude, out metadataExtractorTag) || string.IsNullOrEmpty(metadataExtractorTag.Description))
                    result = null;
                else
                {
                    string longitudeDMS = metadataExtractorTag.Description;
                    double longitude = ParseValueFromDmsString(longitudeDMS);
                    result = new(latitude, longitude);
                    string dms = result.ToDmsString();
                    if ($"{latitudeDMS}, {longitudeDMS}" != dms)
                        result = null;
                }
            }
        }
        return result;
    }

}