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

using Windows.Win32.System.Variant;

namespace Windows.Win32.System.Com;

/// <summary>
///  Helper to scope lifetime of a <see cref="SAFEARRAY"/> created via
///  <see cref="PInvokeCore.SafeArrayCreate(VARENUM, uint, SAFEARRAYBOUND*)"/>
///  Destroys the <see cref="SAFEARRAY"/> (if any) when disposed. Note that this scope currently only works for a
///  one dimensional <see cref="SAFEARRAY"/>.
/// </summary>
/// <remarks>
///  <para>
///   Use in a <see langword="using" /> statement to ensure the <see cref="SAFEARRAY"/> gets disposed.
///  </para>
///  <para>
///   If the <see cref="SAFEARRAY"/> you are intending to scope the lifetime of has type <see cref="VARENUM.VT_UNKNOWN"/>,
///   use <see cref="T:Windows.Win32.System.Com.ComSafeArrayScope`1"/> for better usability.
///  </para>
/// </remarks>
internal readonly unsafe ref struct SafeArrayScope<T>
{
    private readonly nint _value;

    public SAFEARRAY* Value => (SAFEARRAY*)_value;

    public SafeArrayScope(SAFEARRAY* value)
    {
        if (value is null)
        {
            // This SafeArrayScope is meant to receive a SAFEARRAY* from COM.
            _value = (nint)value;
            return;
        }

        if (typeof(T) == typeof(string))
        {
            if (value->VarType is not VARENUM.VT_BSTR)
            {
                throw new ArgumentException($"Wanted SafeArrayScope<{typeof(T)}> but got SAFEARRAY with VarType={value->VarType}");
            }
        }
        else if (typeof(T) == typeof(int))
        {
            if (value->VarType is not VARENUM.VT_I4 and not VARENUM.VT_INT)
            {
                throw new ArgumentException($"Wanted SafeArrayScope<{typeof(T)}> but got SAFEARRAY with VarType={value->VarType}");
            }
        }
        else if (typeof(T) == typeof(double))
        {
            if (value->VarType is not VARENUM.VT_R8)
            {
                throw new ArgumentException($"Wanted SafeArrayScope<{typeof(T)}> but got SAFEARRAY with VarType={value->VarType}");
            }
        }
        else if (typeof(T) == typeof(nint))
        {
            if (value->VarType is not VARENUM.VT_UNKNOWN)
            {
                throw new ArgumentException($"Wanted SafeArrayScope<{typeof(T)}> but got SAFEARRAY with VarType={value->VarType}");
            }
        }
        else if (typeof(T).IsAssignableTo(typeof(IComIID)))
        {
            throw new ArgumentException("Use ComSafeArrayScope instead");
        }
        else if (typeof(T) == typeof(object))
        {
            if (value->VarType is not VARENUM.VT_VARIANT)
            {
                throw new ArgumentException($"Wanted SafeArrayScope<{typeof(T)}> but got SAFEARRAY with VarType={value->VarType}");
            }
        }
        else
        {
            // The type has not been accounted for yet in the SafeArrayScope
            // If the type was intentional, this SafeArrayScope needs to be updated
            // to behave appropriately with this type.
            throw new ArgumentException("Unknown type");
        }

        _value = (nint)value;
    }

    public SafeArrayScope(uint size)
    {
        VARENUM vt;

        if (typeof(T) == typeof(string))
        {
            vt = VARENUM.VT_BSTR;
        }
        else if (typeof(T) == typeof(int))
        {
            vt = VARENUM.VT_I4;
        }
        else if (typeof(T) == typeof(double))
        {
            vt = VARENUM.VT_R8;
        }
        else if (typeof(T) == typeof(nint) || typeof(T).IsAssignableTo(typeof(IComIID)))
        {
            throw new ArgumentException("Use ComSafeArrayScope instead");
        }
        else if (typeof(T) == typeof(object))
        {
            vt = VARENUM.VT_VARIANT;
        }
        else
        {
            // The type has not been accounted for yet in the SafeArrayScope
            // If the type was intentional, this SafeArrayScope needs to be updated
            // to behave appropriately with this type.
            throw new ArgumentException("Unknown type");
        }

        SAFEARRAYBOUND saBound = new()
        {
            cElements = size,
            lLbound = 0
        };

        _value = (nint)PInvokeCore.SafeArrayCreate(vt, 1, &saBound);
        if (_value == 0)
        {
            throw new InvalidOperationException("Unable to create SAFEARRAY");
        }
    }

    public SafeArrayScope(T[] array) : this((uint)array.Length)
    {
        for (int i = 0; i < array.Length; i++)
        {
            this[i] = array[i];
        }
    }

    /// <remarks>
    ///  <para>
    ///   A copy will be made of anything that is put into the <see cref="SAFEARRAY"/>
    ///   and anything the <see cref="SAFEARRAY"/> gives out is a copy and has been add ref appropriately if applicable.
    ///   Be sure to dispose of items that are given to the <see cref="SAFEARRAY"/> if necessary. All
    ///   items given out by the <see cref="SAFEARRAY"/> should be disposed.
    ///  </para>
    /// </remarks>
    public T? this[int i]
    {
        get
        {
            if (typeof(T) == typeof(string))
            {
                using BSTR result = GetElement<BSTR>(i);
                return (T)(object)result.ToString();
            }
            else if (typeof(T) == typeof(int))
            {
                return (T)(object)GetElement<int>(i);
            }
            else if (typeof(T) == typeof(double))
            {
                return (T)(object)GetElement<double>(i);
            }
            else if (typeof(T) == typeof(nint))
            {
                return (T)(object)GetElement<nint>(i);
            }
            else if (typeof(T) == typeof(object))
            {
                using VARIANT result = GetElement<VARIANT>(i);
                return (T?)result.ToObject();
            }

            // Noop. This is an unknown type. We should fill this method out to to do the right
            // thing as we run into new types.
            return default;
        }
        set
        {
            if (Value->VarType == VARENUM.VT_VARIANT)
            {
                using VARIANT variant = VARIANT.FromObject(value);
                PutElement(i, &variant);
            }
            else if (value is string s)
            {
                using BSTR bstrText = new(s);
                PutElement(i, bstrText);
            }
            else if (value is int @int)
            {
                PutElement(i, &@int);
            }
            else if (value is double dbl)
            {
                PutElement(i, &dbl);
            }
            else if (value is nint @nint)
            {
                PutElement(i, (void*)@nint);
            }
        }
    }

    private TReturn GetElement<TReturn>(int index) where TReturn : unmanaged
    {
        Span<int> indices = [index];
        TReturn result;
        fixed (int* pIndices = indices)
        {
            PInvokeCore.SafeArrayGetElement(Value, pIndices, &result).ThrowOnFailure();
        }

        return result;
    }

    private void PutElement(int index, void* value)
    {
        Span<int> indices = [index];
        fixed (int* pIndices = indices)
        {
            PInvokeCore.SafeArrayPutElement((SAFEARRAY*)_value, pIndices, value).ThrowOnFailure();
        }
    }

    public int Length => (int)Value->GetBounds().cElements;

    public bool IsNull => _value == 0;

    public bool IsEmpty => Length == 0;

    public void Dispose()
    {
        SAFEARRAY* safeArray = (SAFEARRAY*)_value;

        // Really want this to be null after disposal to avoid double destroy, but we also want
        // to maintain the readonly state of the struct to allow passing as `in` without creating implicit
        // copies (which would break the T** and void** operators).
        *(void**)this = null;

        if (safeArray is not null)
        {
            PInvokeCore.SafeArrayDestroy(safeArray).ThrowOnFailure();
        }
    }

    public static explicit operator VARIANT(in SafeArrayScope<T> scope) => new() { vt = VARENUM.VT_ARRAY | scope.Value->VarType, data = new() { parray = (SAFEARRAY*)scope._value } };

    public static implicit operator SAFEARRAY*(in SafeArrayScope<T> scope) => (SAFEARRAY*)scope._value;

    public static implicit operator nint(in SafeArrayScope<T> scope) => scope._value;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static implicit operator SAFEARRAY**(in SafeArrayScope<T> scope) => (SAFEARRAY**)Unsafe.AsPointer(ref Unsafe.AsRef(in scope._value));

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static implicit operator void**(in SafeArrayScope<T> scope) => (void**)Unsafe.AsPointer(ref Unsafe.AsRef(in scope._value));
}
