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

using System.Drawing;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;

namespace System.Windows.Forms;

internal static unsafe class AccessibleObjectExtensions
{
    public static ComScope<IAccessible> TryGetIAccessible(this AgileComPointer<IAccessible>? agile, out HRESULT result)
    {
        if (agile is not null)
        {
            return agile.TryGetInterface(out result);
        }

        result = HRESULT.E_NOINTERFACE;
        return default;
    }

    public static Rectangle TryGetLocation(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetLocation((VARIANT)child);

    public static Rectangle TryGetLocation(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        // Use the system provided bounds
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        result = accessible.Value->accLocation(
            out int pxLeft,
            out int pyTop,
            out int pcxWidth,
            out int pcyHeight,
            child);

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetLocation)}: accLocation call for id {(int)child} failed with {result}");
        return new Rectangle(pxLeft, pyTop, pcxWidth, pcyHeight);
    }

    public static BSTR TryGetDefaultAction(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetDefaultAction((VARIANT)child);

    public static BSTR TryGetDefaultAction(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        BSTR bstr = default;
        result = accessible.Value->get_accDefaultAction(child, &bstr);

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetDefaultAction)}: get_accDefaultAction call for id {(int)child} failed with {result}");
        return bstr;
    }

    public static void TryDoDefaultAction(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryDoDefaultAction((VARIANT)child);

    public static void TryDoDefaultAction(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return;
        }

        result = accessible.Value->accDoDefaultAction(child);
    }

    public static BSTR TryGetDescription(this AgileComPointer<IAccessible>? agile, int child) =>
        agile.TryGetDescription((VARIANT)child);

    public static BSTR TryGetDescription(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        BSTR description = default;
        accessible.Value->get_accDescription(child, &description);

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetDescription)}: get_accDescription call for {(int)child} failed with {result}");
        return description;
    }

    public static BSTR TryGetHelp(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetHelp((VARIANT)child);

    public static BSTR TryGetHelp(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        BSTR bstr = default;
        result = accessible.Value->get_accHelp(child, &bstr);

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetHelp)}: get_accHelp call for id {(int)child} failed with {result}");
        return bstr;
    }

    public static BSTR TryGetKeyboardShortcut(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetKeyboardShortcut((VARIANT)child);

    public static BSTR TryGetKeyboardShortcut(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        BSTR bstr = default;
        result = accessible.Value->get_accKeyboardShortcut(child, &bstr);

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetKeyboardShortcut)}: get_accKeyboardShortcut call for id {(int)child} failed with {result}");
        return bstr;
    }

    public static BSTR TryGetName(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetName((VARIANT)child);

    public static BSTR TryGetName(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        BSTR bstr = default;
        result = accessible.Value->get_accName(child, &bstr);

        Debug.Assert(result.Succeeded, $"{nameof(TryGetName)}: get_accName call failed with {result}");
        return bstr;
    }

    public static void TrySetName(this AgileComPointer<IAccessible>? agile, VARIANT child, BSTR name)
    {
        // This is not technically supported any more, unclear if this ever actually does anything.
        // https://learn.microsoft.com/windows/win32/api/oleacc/nf-oleacc-iaccessible-put_accname

        if (name.IsNull)
        {
            return;
        }

        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return;
        }

        result = accessible.Value->put_accName(child, name);

        Debug.WriteLineIf(result.Failed, $"{nameof(TrySetName)}: put_accName call failed with {result}");
    }

    public static AccessibleRole TryGetRole(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetRole((VARIANT)child);

    public static AccessibleRole TryGetRole(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return AccessibleRole.None;
        }

        using VARIANT role = default;
        result = accessible.Value->get_accRole(child, &role);

        Debug.Assert(result.Succeeded, $"{nameof(TryGetRole)}: get_accRole call failed with {result}");

        return role.vt is VARENUM.VT_I4 or VARENUM.VT_INT ? (AccessibleRole)(int)role : AccessibleRole.None;
    }

    public static AccessibleStates TryGetState(this AgileComPointer<IAccessible>? agile, int child)
        => agile.TryGetState((VARIANT)child);

    public static AccessibleStates TryGetState(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return AccessibleStates.None;
        }

        using VARIANT state = default;
        result = accessible.Value->get_accState(child, &state);

        Debug.Assert(
            result.Succeeded,
            $"{nameof(TryGetState)}: get_accState call for id {(int)child} failed with {result}");

        return state.vt is VARENUM.VT_I4 or VARENUM.VT_INT ? (AccessibleStates)(int)state : AccessibleStates.None;
    }

    public static BSTR TryGetValue(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        BSTR bstr = default;
        result = accessible.Value->get_accValue(child, &bstr);

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetValue)}: get_accValue call failed with {result}");
        return bstr;
    }

    public static void TrySetValue(this AgileComPointer<IAccessible>? agile, VARIANT child, BSTR value)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return;
        }

        result = accessible.Value->put_accValue(child, value);

        if (result.Failed && result != HRESULT.DISP_E_MEMBERNOTFOUND)
        {
            result.ThrowOnFailure();
        }

        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TrySetValue)}: put_accValue call failed with {result}");
    }

    public static (int topic, BSTR helpFile) TryGetHelpTopic(this AgileComPointer<IAccessible>? agile, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return (-1, default);
        }

        BSTR file = default;
        result = accessible.Value->get_accHelpTopic(out file, child, out int topicId);
        Debug.Assert(
            result.Succeeded || result == HRESULT.DISP_E_MEMBERNOTFOUND,
            $"{nameof(TryGetHelpTopic)}: put_accValue call failed with {result}");

        return result.Failed ? (-1, file) : (topicId, file);
    }

    public static void TrySelect(this AgileComPointer<IAccessible>? agile, AccessibleSelection flags, int child)
        => agile.TrySelect(flags, (VARIANT)child);

    public static void TrySelect(this AgileComPointer<IAccessible>? agile, AccessibleSelection flags, VARIANT child)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return;
        }

        result = accessible.Value->accSelect((int)flags, child);
    }

    public static int TryGetChildCount(this AgileComPointer<IAccessible>? agile)
    {
        using var accessible = agile.TryGetIAccessible(out HRESULT result);
        if (result.Failed)
        {
            return default;
        }

        result = accessible.Value->get_accChildCount(out int count);
        return count;
    }

    /// <summary>
    ///  Gets the accessible object's owner control's accessible role.
    /// </summary>
    public static AccessibleRole GetOwnerAccessibleRole<TAccessible>(
        this TAccessible accessibleObject,
        AccessibleRole defaultRole = AccessibleRole.Default)
        where TAccessible : AccessibleObject, IOwnedObject<Control>
    {
        AccessibleRole role = accessibleObject.Owner?.AccessibleRole ?? AccessibleRole.Default;
        return role == AccessibleRole.Default ? defaultRole : role;
    }

    /// <summary>
    ///  Gets the accessible object's owner control's accessible name.
    /// </summary>
    [return: NotNullIfNotNull(nameof(defaultName))]
    public static string? GetOwnerAccessibleName<TAccessible>(
        this TAccessible accessibleObject,
        string? defaultName = null) where TAccessible : AccessibleObject, IOwnedObject<Control>
        => accessibleObject.Owner?.AccessibleName ?? defaultName;

    /// <summary>
    ///  Gets the accessible object's owner control's accessible text.
    /// </summary>
    public static string GetOwnerText<TAccessible>(this TAccessible accessibleObject, string defaultText = "")
        where TAccessible : AccessibleObject, IOwnedObject<Control>
        => accessibleObject.Owner?.Text ?? defaultText;
}
