using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace NixUniversalSDK.Wrapper;

/// <summary>
/// Defines shared constants used in the wrapper
/// </summary>
public static class Constants
{
    /// <summary>
    /// String value returned when serializing an empty / null object to JSON
    /// </summary>
    public const string EmptyJson = "{}";

    /// <summary>
    /// Value reported for temperature measurement error
    /// </summary>
    public const float TemperatureError = -2048;
}

/// <summary>
/// Utility functions
/// </summary>
internal static class Utils
{
    /// <summary>
    /// Copies characters up to the first null to a managed string.
    /// </summary>
    internal static string MarshalPointerAsString(nint ptr)
    {
        string? result = null;
        if (ptr != nint.Zero)
        {
            result = Marshal.PtrToStringAnsi(ptr);
        }
        return result ?? string.Empty;
    }

    /// <summary>
    /// Copies a managed string into unmanaged memory.
    /// </summary>
    internal static nint MarshalStringToPointer(string? value)
    {
        return Marshal.StringToHGlobalAnsi(value ?? string.Empty);
    }

    /// <summary>
    /// Converts an unmanaged pointer to a managed delegate function.
    /// </summary>
    internal static TDelegate? GetDelegateForFunctionPointer<TDelegate>(nint ptr)
    {
        if (ptr != nint.Zero)
        {
            return Marshal.GetDelegateForFunctionPointer<TDelegate>(ptr);
        }
        else
        {
            return default;
        }
    }
    
    /// <summary>
    /// Formats an object as a JSON string using <see cref="JsonSerializerDefaults.Web"/>. Member names are serialized with camel-case formatting.
    /// </summary>
    internal static string AsJsonString<T>(this T value)
    {
        var result = Constants.EmptyJson;       
        if (value is not null)
        {
            try
            {
                result = JsonSerializer.Serialize(
                    value,
                    typeof(T),
                    SourceGenerationContext.Default);
            } 
            catch
            {
                // Serialization failed
                result = Constants.EmptyJson;
            }
        }
        return result;
    }

    /// <summary>
    /// Converts <see cref="DateTime"/> to Java 'ticks' (ms since Unix epoch)
    /// </summary>
    internal static ulong GetJavaTicks(this DateTime dateTime)
    {
        var ms = dateTime.ToUniversalTime().Subtract(
                new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
                .TotalMilliseconds;
        return (ulong)Math.Round(ms);
    }

    /// <summary>
    /// Converts <see cref="DateTime"/> to Java 'ticks' (ms since Unix epoch)
    /// </summary>
    internal static ulong GetJavaTicks(this DateTime? dateTime) =>
        dateTime?.GetJavaTicks() ?? 0;

    /// <summary>
    /// Converts a set of <see cref="DeviceType"/> objects to a compacted JSON list keeping only each entry's integer value.
    /// </summary>
    internal static string DeviceTypesToJson(ISet<DeviceType> types)
    {
        var ints = new List<int>();
        foreach (var type in types.ToArray().OrderBy(x => ((byte)x)))
            ints.Add((int)type);
        return ints.ToArray().AsJsonString();
    }

    /// <summary>
    /// Converts a set of <see cref="LicenseFeature"/> objects to a compacted JSON list keeping only each entry's integer value.
    /// </summary>
    internal static string LicenseFeaturesToJson(ISet<LicenseFeature> features)
    {
        var ints = new List<int>();
        foreach (var feature in features.ToArray().OrderBy(x => ((int)x)))
            ints.Add((int)feature);
        return ints.ToArray().AsJsonString();
    }

    /// <summary>
    /// Current version of the Nix Universal SDK.
    /// </summary>
    public static string WrapperVersion => Assembly
        .GetExecutingAssembly()
        .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
        ?.InformationalVersion
        ?.ToString() ?? string.Empty;
}

/// <summary>
/// Defines possible callback function types
/// </summary>
public static class Delegates
{
    /// <summary>
    /// Delegate definition for a callback with no arguments
    /// </summary>
    /// <param name="senderName">Name of the event that invoked this callback</param>
    /// <example>
    /// The equivalent C function pointer for this delegate is:
    /// <code>
    /// void (*callback)(const char*)
    /// </code>
    /// </example>
    public delegate void Empty(string senderName);

    /// <summary>
    /// Delegate definition for a callback with a boolean argument
    /// </summary>
    /// <param name="senderName">Name of the event that invoked this callback</param>
    /// <param name="boolValue">Boolean value for this event</param>
    /// <example>
    /// The equivalent C function pointer for this delegate is:
    /// <code>
    /// void (*callback)(const char*, bool)
    /// </code>
    /// </example>
    public delegate void BoolValue(
        string senderName,
        bool boolValue);

    /// <summary>
    /// Delegate definition for a callback with an integer argument
    /// </summary>
    /// <param name="senderName">Name of the event that invoked this callback</param>
    /// <param name="intValue">Integer value for this event</param>
    /// <example>
    /// The equivalent C function pointer for this delegate is:
    /// <code>
    /// void (*callback)(const char*, int32_t)
    /// </code>
    /// </example>
    public delegate void IntValue(
        string senderName,
        int intValue);

    /// <summary>
    /// Delegate definition for a callback with a string argument
    /// </summary>
    /// <param name="senderName">Name of the event that invoked this callback</param>
    /// <param name="stringValue">String value for this event</param>
    /// <example>
    /// The equivalent C function pointer for this delegate is:
    /// <code>
    /// void (*callback)(const char*, const char*)
    /// </code>
    /// </example>
    public delegate void StringValue(
        string senderName,
        string stringValue);
}

/// <summary>
/// Simple class to contain basic description of <see cref="IDeviceCompat"/> when found by the <see cref="IDeviceScanner"/>, used in callbacks
/// </summary>
internal class ScannerResult
{
    /// <summary>
    /// Device ID from scan result.
    /// </summary>
    public required string Id { get; set; }

    /// <summary>
    /// Device interface type from scan result. Integer value is one of <see cref="NixUniversalSDK.InterfaceType"/>.
    /// </summary>
    public required int InterfaceType { get; set; }

    /// <summary>
    /// Device name from scan result.
    /// </summary>
    public required string Name { get; set; }

    /// <summary>
    /// Device signal strength / RSSI from scan result.
    /// </summary>
    public required int Rssi { get; set; }

    /// <summary>
    /// Device type from scan result. Integer value is one of <see cref="NixUniversalSDK.DeviceType"/>.
    /// </summary>
    public required int Type { get; set; }
}

/// <summary>
/// Measurement metadata summary used when calling <see cref="Exported.Measurement_GetLastMetadataJson(nint)"/>
/// </summary>
internal class MeasurementMetadata
{
    /// <summary>
    /// Time stamp of this measurement, represented as a <see cref="UInt64">UInt64</see>. This corresponds to the milliseconds since the Unix epoch (January 1, 1970 00:00 UTC).
    /// </summary>
    public required ulong DateMs { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.Status"/>
    /// </summary>
    public required byte Status { get; set; }

    /// <summary>
    /// Corresponds to the full name of <see cref="IMeasurementData.DeviceType"/>
    /// </summary>
    public required string DeviceName { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.DeviceType"/> formatted as a byte
    /// </summary>
    public required byte DeviceType { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.Mode"/>, formatted as a name / string
    /// </summary>
    public required string Mode { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.TRef"/>
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public float? TRef { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.TScan"/>
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public float? TScan { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.TReal"/>
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public bool? TReal { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.TCompEnabled"/>
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public bool? TCompEnabled { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IMeasurementData.TileEnabled"/>
    /// </summary>
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public bool? TileEnabled { get; set; }
}

/// <summary>
/// Simple class used to serialize <see cref="ISpectralData"/> to JSON
/// </summary>
internal class SpectralDataSerializable
{
    /// <summary>
    /// Corresponds to <see cref="ISpectralData.Mode"/>, formatted as a name / string
    /// </summary>
    public required string Mode { get; set; }

    /// <summary>
    /// Minimum / starting wavelength value in nm for <see cref="Value"/>
    /// </summary>
    public required int LambdaMin { get; set; }

    /// <summary>
    /// Wavelength interval in nm for <see cref="Value"/>
    /// </summary>
    public required int LambdaInterval { get; set; }

    /// <summary>
    /// Corresponds to <see cref="ISpectralData.Value"/>
    /// </summary>
    public required float[] Value { get; set; }
}

/// <summary>
/// Simple class used to serialize <see cref="IDensityData"/> to JSON
/// </summary>
internal class DensityDataSerializable
{
    /// <summary>
    /// Corresponds to <see cref="IDensityData.Mode"/>, formatted as a name / string
    /// </summary>
    public required string Mode { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IDensityData.StatusLabel"/> 
    /// </summary>
    public required string IsoStatus { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IDensityData.AutoIndex"/>
    /// </summary>
    public required int AutoIndex { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IDensityData.Value"/>
    /// </summary>
    public required double[] Value { get; set; }
}

/// <summary>
/// Simple class used to serialize <see cref="IColorData"/> to JSON
/// </summary>
internal class ColorDataSerializable
{       
    /// <summary>
    /// Corresponds to <see cref="IColorData.Mode"/>, formatted as a name / string
    /// </summary>
    public required string Mode { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IColorData.Reference"/>, formatted as a name / string
    /// </summary>
    public required string Reference { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IColorData.Type"/>, formatted as a name / string
    /// </summary>
    public required string Type { get; set; }

    /// <summary>
    /// Corresponds to <see cref="IColorData.Value"/>
    /// </summary>
    public required double[] Value { get; set; }
}

/// <summary>
/// Simple class used to serialize raw data from <see cref="DeviceResult"/> objects to JSON
/// </summary>
internal class DeviceResultDebug
{
    /// <summary>
    /// Corresponds to <see cref="DeviceResult.Status"/>, formatted as an integer
    /// </summary>
    public required int Status { get; set; }

    /// <summary>
    /// Corresponds to <see cref="DeviceResult.Measurements"/>. Key-value pairs correspond to the <see cref="IMeasurementData.Raw"/> values for each available scan mode.
    /// </summary>
    public required IDictionary<string, string> Measurements { get; set; }
}

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(int[]))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(ScannerResult[]))]
[JsonSerializable(typeof(MeasurementMetadata))]
[JsonSerializable(typeof(SpectralDataSerializable))]
[JsonSerializable(typeof(DensityDataSerializable))]
[JsonSerializable(typeof(ColorDataSerializable))]    
[JsonSerializable(typeof(DeviceResultDebug))]    
internal partial class SourceGenerationContext : JsonSerializerContext 
{
}

/// <summary>
/// Extensions for <see cref="IDeviceCompat"/> specific to the static wrapper.
/// </summary>
internal static class DeviceCompatExtensions
{
    /// <summary>
    /// Creates a <see cref="ScannerResult"/> from a <see cref="IDeviceCompat"/> instance
    /// </summary>
    public static ScannerResult? AsScannerResult(this IDeviceCompat device)
    {
        if (device is not null)
        {
            return new ScannerResult
            {
                Id = device.Id,
                InterfaceType = (int)device.InterfaceType,
                Name = device.Name,
                Rssi = device.Rssi,
                Type = (int)device.Type
            };
        }
        else
        {
            return null;
        }
    }
}

internal static class MeasurementDataExtensions
{
    /// <summary>
    /// Gets <see cref="MeasurementMetadata"/> for a <see cref="IMeasurementData"/> instance
    /// </summary>
    public static MeasurementMetadata? GetMetadata(
        this IMeasurementData data, 
        DateTime? dateTime)
    {
        if (data is not null) 
        {
            return new MeasurementMetadata
            {
                DateMs = dateTime.GetJavaTicks(),
                Status = data.Status,
                DeviceName = data.DeviceType.GetFullName(),
                DeviceType = (byte)data.DeviceType,
                Mode = data.Mode.ToString(),
                TRef = data.TRef,
                TScan = data.TScan,
                TReal = data.TReal,
                TCompEnabled = data.TCompEnabled,
                TileEnabled = data.TileEnabled
            };
        }
        else
        {
            return null;
        }
    }

    /// <summary>
    /// Converts <see cref="ISpectralData"/> to <see cref="SpectralDataSerializable"/>
    /// </summary>
    public static SpectralDataSerializable? GetSerializable(
        this ISpectralData data)
    {
        if (data is not null) 
        {
            return new SpectralDataSerializable
            {
                Mode = data.Mode.ToString(),
                LambdaMin = data.Lambda.Min(),
                LambdaInterval = data.Interval,
                Value = data.Value
            };
        }
        else 
        {
            return null;
        }
    }

    /// <summary>
    /// Converts <see cref="IDensityData"/> to <see cref="DensityDataSerializable"/>
    /// </summary>
    public static DensityDataSerializable? GetSerializable(
        this IDensityData data)
    {
        if (data is not null) 
        {
            return new DensityDataSerializable
            {
                Mode = data.Mode.ToString(),
                IsoStatus = data.Status.ToString(),
                AutoIndex = data.AutoIndex,
                Value = data.Value
            };
        }
        else 
        {
            return null;
        }
    }

    /// <summary>
    /// Converts <see cref="IColorData"/> to <see cref="ColorDataSerializable"/>
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    public static ColorDataSerializable? GetSerializable(
        this IColorData data)
    {
        if (data is not null) 
        {
            return new ColorDataSerializable
            {
                Mode = data.Mode.ToString(),
                Reference = data.Reference.ToString(),
                Type = data.Type.ToString(),
                Value = data.Value
            };
        }
        else
        {
            return null;
        }
    }

    /// <summary>
    /// Converts <see cref="DeviceResult"/> to <see cref="DeviceResultDebug"/>
    /// </summary>
    public static DeviceResultDebug? GetDebug(
        this DeviceResult result)
    {
        if (result is not null)
        {
            var measurements = new SortedDictionary<string, string>();
            if (result.Measurements is Dictionary<ScanMode, IMeasurementData> dictionary)
            {
                foreach (ScanMode mode in dictionary.Keys)
                {
                    measurements[mode.ToString()] = dictionary[mode].Raw;
                }
            }
            return new DeviceResultDebug
            {
                Status = (int)result.Status,
                Measurements = measurements
            };
        }
        else
        {
            return null;
        }
    }
}
