// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;

namespace System.Private.Windows;

/// <summary>
///  Wrapper which contains JSON serialized data along with the JSON data's original type information
///  to be deserialized later.
/// </summary>
/// <remarks>
///  <para>
///   There may be instances where this type is not available in different versions, e.g. .NET 8, .NET Framework.
///   If this type needs to be deserialized from stream in these instances, a workaround would be to create an assembly with the name <see cref="IJsonData.CustomAssemblyName"/>
///   and replicate this type. Then, manually retrieve the serialized stream and use the <see cref="Formats.Nrbf.NrbfDecoder"/> to decode the stream and rehydrate the serialized type.
///   Alternatively, but not recommended, BinaryFormatter can also be used to deserialize the stream if this type is not available.
///  </para>
/// </remarks>
/// <example>
/// <![CDATA[
///  // Recommended: deserialize using NrbfDecoder.
///  void DeserializeJsonData(DataObject dataObject)
///  {
///     // Manually retrieve serialized stream.
///     System.Runtime.InteropServices.ComTypes.FORMATETC formatetc = new()
///     {
///         cfFormat = (short)DataFormats.GetFormat("yourDataFormat").Id,
///         dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT,
///         lindex = -1,
///         tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL
///     };
///
///     System.Runtime.InteropServices.ComTypes.IDataObject castedDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)dataObject;
///     castedDataObject.GetData(ref formatetc, out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium);
///     IntPtr hglobal = medium.unionmember;
///     Stream stream;
///     try
///     {
///         IntPtr buffer = GlobalLock(hglobal);
///         int size = GlobalSize(hglobal);
///         byte[] bytes = new byte[size];
///         Marshal.Copy(buffer, bytes, 0, size);
///         // this comes from DataObject.Composition.s_serializedObjectID
///         int index = 16;
///         stream = new MemoryStream(bytes, index, bytes.Length - index);
///     }
///     finally
///     {
///         GlobalUnlock(hglobal);
///     }
///
///     // Use Nrbf to decode stream and rehydrate data.
///     System.Formats.Nrbf.SerializationRecord record = System.Formats.Nrbf.NrbfDecoder.Decode(stream);
///     if (record.TypeName.AssemblyName.FullName != "System.Private.Windows.VirtualJson")
///     {
///         // The data was not serialized as JSON.
///         return;
///     }
///
///     if (record is not System.Formats.Nrbf.ClassRecord types
///         || types.GetRawValue("<JsonBytes>k__BackingField") is not System.Formats.Nrbf.SZArrayRecord<byte> byteData
///         || types.GetRawValue("<InnerTypeAssemblyQualifiedName>k__BackingField") is not string innerTypeAssemblyQualifiedName
///         || !System.Reflection.Metadata.TypeName.TryParse(innerTypeAssemblyQualifiedName.ToCharArray(), out System.Reflection.Metadata.TypeName? innerTypeName))
///     {
///         // This is supposed to be JsonData, but somehow the data is corrupt.
///         throw new InvalidOperationException();
///     }
///
///     // TODO: Additional checking on 'innerTypeName' to ensure it is expected type.
///
///     // This should return the original data that was JSON serialized.
///     var result = System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), innerType);
///     // TODO: Process the payload as needed.
///  }
///
///  [DllImport("kernel32.dll")]
///  static extern int GlobalSize(IntPtr hMem);
///
///  [DllImport("kernel32.dll")]
///  static extern IntPtr GlobalLock(IntPtr hMem);
///
///  [DllImport("kernel32.dll")]
///  static extern int GlobalUnlock(IntPtr hMem);
///
///  // OR
///  // Not recommended: deserialize using BinaryFormatter.
///
///  // This definition must live in an assembly named System.Private.Windows.VirtualJson and referenced in order to work as expected.
///  namespace System.Private.Windows;
///  [Serializable]
///  struct JsonData : IObjectReference
///  {
///     public byte[] JsonBytes { get; set; }
///
///     public string InnerTypeAssemblyQualifiedName { get; set; }
///
///     public object GetRealObject(StreamingContext context)
///     {
///         // TODO: Additional checking on InnerTypeAssemblyQualifiedName to ensure it is expected type.
///         return JsonSerializer.Deserialize(JsonBytes, typeof(ExpectedType)) ?? throw new InvalidOperationException();
///     }
///  }
///  ]]>
/// </example>
[Serializable]
internal struct JsonData<T> : IJsonData
{
    public byte[] JsonBytes { get; set; }

    public readonly string InnerTypeAssemblyQualifiedName => typeof(T).ToTypeName().AssemblyQualifiedName;

    [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize<TValue>(ReadOnlySpan<Byte>, JsonSerializerOptions)")]
    public readonly object Deserialize()
    {
        object? result;
        try
        {
            result = JsonSerializer.Deserialize<T>(JsonBytes);
        }
        catch (Exception ex)
        {
            result = new NotSupportedException(ex.Message);
        }

        return result ?? throw new InvalidOperationException();
    }
}

/// <summary>
///  Represents an object that contains JSON serialized data. This interface is used to
///  identify a <see cref="JsonData{T}"/> without needing to have the generic type information.
/// </summary>
internal interface IJsonData
{
    // We use a custom assembly name to allow versions where JsonData<T> doesn't exist to still be able rehydrate JSON serialized data.
    const string CustomAssemblyName = "System.Private.Windows.VirtualJson";

    byte[] JsonBytes { get; set; }

    /// <summary>
    ///  The assembly qualified name of the T in <see cref="JsonData{T}"/>. This name should
    ///  have any <see cref="TypeForwardedFromAttribute"/> names taken into account.
    /// </summary>
    string InnerTypeAssemblyQualifiedName { get; }

    /// <summary>
    ///  Deserializes the data stored in the JsonData. This is a convenience method
    ///  to deserialize the data when we are not dealing with a binary formatted record.
    /// </summary>
    [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize<TValue>(ReadOnlySpan<Byte>, JsonSerializerOptions)")]
    object Deserialize();

    [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.SerializeToUtf8Bytes<TValue>(TValue, JsonSerializerOptions)")]
    internal static IJsonData Create<T>(T data) => new JsonData<T>() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) };
}
